From 84cf17b9e6d3236ea375410dd8b3b5f6c46c2354 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 30 Apr 2026 12:29:07 +0200 Subject: [PATCH 1/6] feat: Add `@cursor/sdk` instrumentation --- .env.example | 1 + .github/workflows/e2e-canary.yaml | 1 + .github/workflows/integration-tests.yaml | 3 + e2e/README.md | 1 + e2e/config/pr-comment-scenarios.json | 6 + e2e/helpers/scenario-installer.ts | 37 +- .../cursor-sdk-v1-auto-hook.span-events.json | 203 +++ .../cursor-sdk-v1-wrapped.span-events.json | 203 +++ .../cursor-sdk-instrumentation/assertions.ts | 246 ++++ .../cursor-sdk-instrumentation/package.json | 20 + .../cursor-sdk-instrumentation/pnpm-lock.yaml | 1168 ++++++++++++++++ .../scenario.cursor-sdk-v1.mjs | 5 + .../scenario.cursor-sdk-v1.ts | 5 + .../scenario.impl.mjs | 143 ++ .../scenario.test.ts | 56 + e2e/scripts/run-canary-tests-docker.mjs | 1 + .../auto-instrumentations/bundler/plugin.ts | 2 + .../bundler/webpack-loader.ts | 2 + .../configs/cursor-sdk.ts | 49 + js/src/auto-instrumentations/hook.mts | 4 + js/src/auto-instrumentations/index.ts | 1 + js/src/exports.ts | 1 + js/src/instrumentation/braintrust-plugin.ts | 14 + .../plugins/cursor-sdk-channels.ts | 47 + .../plugins/cursor-sdk-plugin.test.ts | 259 ++++ .../plugins/cursor-sdk-plugin.ts | 1179 +++++++++++++++++ js/src/instrumentation/registry.ts | 10 +- js/src/vendor-sdk-types/cursor-sdk.ts | 337 +++++ js/src/wrappers/cursor-sdk.test.ts | 145 ++ js/src/wrappers/cursor-sdk.ts | 182 +++ turbo.json | 8 + 31 files changed, 4306 insertions(+), 33 deletions(-) create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/assertions.ts create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/package.json create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.mjs create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.ts create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs create mode 100644 e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts create mode 100644 js/src/auto-instrumentations/configs/cursor-sdk.ts create mode 100644 js/src/instrumentation/plugins/cursor-sdk-channels.ts create mode 100644 js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts create mode 100644 js/src/instrumentation/plugins/cursor-sdk-plugin.ts create mode 100644 js/src/vendor-sdk-types/cursor-sdk.ts create mode 100644 js/src/wrappers/cursor-sdk.test.ts create mode 100644 js/src/wrappers/cursor-sdk.ts diff --git a/.env.example b/.env.example index c9c314bb3..bff0c17ab 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ BRAINTRUST_API_KEY= OPENAI_API_KEY= ANTHROPIC_API_KEY= GEMINI_API_KEY= +CURSOR_API_KEY= OPENROUTER_API_KEY= MISTRAL_API_KEY= HUGGINGFACE_API_KEY= diff --git a/.github/workflows/e2e-canary.yaml b/.github/workflows/e2e-canary.yaml index 12a287004..728c4f069 100644 --- a/.github/workflows/e2e-canary.yaml +++ b/.github/workflows/e2e-canary.yaml @@ -35,6 +35,7 @@ jobs: BRAINTRUST_E2E_PROJECT_NAME: ${{ vars.BRAINTRUST_E2E_PROJECT_NAME }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 7b7b88ef3..5e5cfcbbe 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -29,6 +29,7 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} HUGGINGFACE_API_KEY: ${{ secrets.HUGGINGFACE_API_KEY }} @@ -58,6 +59,7 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} @@ -110,6 +112,7 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} diff --git a/e2e/README.md b/e2e/README.md index b4a15d56e..3ed3d4b1b 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -115,6 +115,7 @@ Non-hermetic scenarios require provider credentials in addition to the mock Brai - `OPENAI_API_KEY` - `ANTHROPIC_API_KEY` - `GEMINI_API_KEY` or `GOOGLE_API_KEY` +- `CURSOR_API_KEY` - `OPENROUTER_API_KEY` - `MISTRAL_API_KEY` - `HUGGINGFACE_API_KEY` diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index bee0040c5..0108b9e78 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -133,5 +133,11 @@ "label": "v0.2.81" } ] + }, + { + "scenarioDirName": "cursor-sdk-instrumentation", + "label": "Cursor SDK Instrumentation", + "metadataScenario": "cursor-sdk-instrumentation", + "variants": [{ "variantKey": "cursor-sdk-v1", "label": "v1" }] } ] diff --git a/e2e/helpers/scenario-installer.ts b/e2e/helpers/scenario-installer.ts index 29318aeff..9eb7b19c2 100644 --- a/e2e/helpers/scenario-installer.ts +++ b/e2e/helpers/scenario-installer.ts @@ -22,6 +22,7 @@ const INSTALL_SECRET_ENV_VARS = [ "ANTHROPIC_API_KEY", "BRAINTRUST_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GEMINI_API_KEY", "GITHUB_TOKEN", "GH_TOKEN", @@ -37,10 +38,9 @@ let cleanupRegistered = false; type CanaryDependencyRule = { packageName: string; - query: string; + version: string; }; -const canaryVersionCache = new Map(); const HELPERS_DIR = path.dirname(fileURLToPath(import.meta.url)); const E2E_ROOT = path.resolve(HELPERS_DIR, ".."); @@ -155,32 +155,6 @@ function packageSpecifier( : `npm:${packageName}@${version}`; } -async function resolveCanaryVersion( - rule: CanaryDependencyRule, -): Promise { - const cacheKey = rule.query; - const cached = canaryVersionCache.get(cacheKey); - if (cached) { - return cached; - } - - const output = await spawnOrThrow( - PNPM_COMMAND, - ["view", rule.query, "version", "--json"], - process.cwd(), - installEnv(), - ); - const parsed = JSON.parse(output) as string | string[]; - const version = Array.isArray(parsed) ? parsed.at(-1) : parsed; - - if (typeof version !== "string") { - throw new Error(`Could not resolve canary version for ${rule.query}`); - } - - canaryVersionCache.set(cacheKey, version); - return version; -} - function parseCanaryDependencyRule( dependencyName: string, rawRule: string, @@ -195,7 +169,7 @@ function parseCanaryDependencyRule( if (rawRule === "latest") { return { packageName: dependencyName, - query: dependencyName, + version: "latest", }; } @@ -208,7 +182,7 @@ function parseCanaryDependencyRule( return { packageName: rawRule.slice(0, versionSeparator), - query: rawRule, + version: rawRule.slice(versionSeparator + 1), }; } @@ -230,11 +204,10 @@ async function rewriteManifestForCanary(scenarioDir: string): Promise { rawRule, scenarioDir, ); - const version = await resolveCanaryVersion(rule); dependencies[dependencyName] = packageSpecifier( dependencyName, rule.packageName, - version, + rule.version, ); updated = true; } diff --git a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json new file mode 100644 index 000000000..1d38061df --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json @@ -0,0 +1,203 @@ +{ + "conversation": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "resume-conversation" + }, + "metric_keys": [], + "name": "cursor-sdk-resume-conversation-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + }, + "prompt": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "prompt" + }, + "metric_keys": [], + "name": "cursor-sdk-prompt-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "Agent.prompt", + "cursor_sdk.run_id": "", + "cursor_sdk.runtime": "local", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + }, + "root": { + "has_input": false, + "has_output": false, + "metadata": { + "scenario": "cursor-sdk-instrumentation" + }, + "metric_keys": [], + "name": "cursor-sdk-root", + "root_span_id": "", + "span_id": "", + "span_parents": [], + "type": "task" + }, + "stream": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "stream" + }, + "metric_keys": [], + "name": "cursor-sdk-stream-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "subagent_task": null, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + "tool": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "shell" + }, + "metric_keys": [], + "name": "tool: shell", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "tool" + } + }, + "wait": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "wait" + }, + "metric_keys": [], + "name": "cursor-sdk-wait-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "cursor_sdk.step_types": [ + "assistantMessage" + ], + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "completion_tokens", + "cursor_sdk.delta_tokens", + "cursor_sdk.step_duration_ms", + "cursor_sdk.steps", + "duration", + "prompt_cache_creation_tokens", + "prompt_cached_tokens", + "prompt_tokens", + "tokens" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + } +} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json new file mode 100644 index 000000000..1d38061df --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json @@ -0,0 +1,203 @@ +{ + "conversation": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "resume-conversation" + }, + "metric_keys": [], + "name": "cursor-sdk-resume-conversation-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + }, + "prompt": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "prompt" + }, + "metric_keys": [], + "name": "cursor-sdk-prompt-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "Agent.prompt", + "cursor_sdk.run_id": "", + "cursor_sdk.runtime": "local", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + }, + "root": { + "has_input": false, + "has_output": false, + "metadata": { + "scenario": "cursor-sdk-instrumentation" + }, + "metric_keys": [], + "name": "cursor-sdk-root", + "root_span_id": "", + "span_id": "", + "span_parents": [], + "type": "task" + }, + "stream": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "stream" + }, + "metric_keys": [], + "name": "cursor-sdk-stream-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "subagent_task": null, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "duration" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + "tool": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "shell" + }, + "metric_keys": [], + "name": "tool: shell", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "tool" + } + }, + "wait": { + "operation": { + "has_input": false, + "has_output": false, + "metadata": { + "operation": "wait" + }, + "metric_keys": [], + "name": "cursor-sdk-wait-operation", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": null + }, + "task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.agent_id": "", + "cursor_sdk.model": "composer-2", + "cursor_sdk.operation": "agent.send", + "cursor_sdk.run_id": "", + "cursor_sdk.status": "finished", + "cursor_sdk.step_types": [ + "assistantMessage" + ], + "model": "composer-2", + "provider": "cursor" + }, + "metric_keys": [ + "completion_tokens", + "cursor_sdk.delta_tokens", + "cursor_sdk.step_duration_ms", + "cursor_sdk.steps", + "duration", + "prompt_cache_creation_tokens", + "prompt_cached_tokens", + "prompt_tokens", + "tokens" + ], + "name": "Cursor Agent", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + } + } +} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts new file mode 100644 index 000000000..16363d8b8 --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts @@ -0,0 +1,246 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import { normalizeForSnapshot, type Json } from "../../helpers/normalize"; +import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; +import { + formatJsonFileSnapshot, + resolveFileSnapshotPath, +} from "../../helpers/file-snapshot"; +import { withScenarioHarness } from "../../helpers/scenario-harness"; +import { + findAllSpans, + findChildSpans, + findLatestSpan, +} from "../../helpers/trace-selectors"; +import { summarizeWrapperContract } from "../../helpers/wrapper-contract"; +import { ROOT_NAME, SCENARIO_NAME } from "./scenario.impl.mjs"; + +type RunCursorSDKScenario = (harness: { + runNodeScenarioDir: (options: { + entry: string; + nodeArgs: string[]; + runContext?: { variantKey: string }; + scenarioDir: string; + timeoutMs: number; + }) => Promise; + runScenarioDir: (options: { + entry: string; + runContext?: { variantKey: string }; + scenarioDir: string; + timeoutMs: number; + }) => Promise; +}) => Promise; + +const METADATA_KEYS = [ + "provider", + "model", + "operation", + "scenario", + "gen_ai.tool.name", + "cursor_sdk.model", + "cursor_sdk.operation", + "cursor_sdk.agent_id", + "cursor_sdk.run_id", + "cursor_sdk.runtime", + "cursor_sdk.status", + "cursor_sdk.duration_ms", + "cursor_sdk.step_types", + "cursor_sdk.tool.status", +] as const; + +function summarizeSpan(event: CapturedLogEvent | undefined): Json { + if (!event) { + return null; + } + const summary = summarizeWrapperContract(event, [...METADATA_KEYS]) as Record< + string, + Json + >; + if (summary.metadata && typeof summary.metadata === "object") { + const metadata = summary.metadata as Record; + if (typeof metadata["cursor_sdk.agent_id"] === "string") { + metadata["cursor_sdk.agent_id"] = ""; + } + if (typeof metadata["cursor_sdk.run_id"] === "string") { + metadata["cursor_sdk.run_id"] = ""; + } + if (typeof metadata["cursor_sdk.duration_ms"] === "number") { + metadata["cursor_sdk.duration_ms"] = 1; + } + } + if (typeof event.row.error === "string") { + summary.error = event.row.error; + } + return summary; +} + +function findOperation(events: CapturedLogEvent[], name: string) { + return findLatestSpan(events, name); +} + +function findCursorTask(events: CapturedLogEvent[], operationName: string) { + const operation = findOperation(events, operationName); + return findChildSpans(events, "Cursor Agent", operation?.span.id).at(-1); +} + +function outputText(event: CapturedLogEvent | undefined): string { + return typeof event?.output === "string" ? event.output : ""; +} + +function summarize(events: CapturedLogEvent[]): Json { + const promptTask = findCursorTask(events, "cursor-sdk-prompt-operation"); + const streamTask = findCursorTask(events, "cursor-sdk-stream-operation"); + const waitTask = findCursorTask(events, "cursor-sdk-wait-operation"); + const conversationTask = findCursorTask( + events, + "cursor-sdk-resume-conversation-operation", + ); + const tool = findAllSpans(events, "tool: shell").at(-1); + const subagentTask = events.find( + (event) => + event.span.type === "task" && event.span.name?.startsWith("Agent:"), + ); + + return normalizeForSnapshot({ + conversation: { + operation: summarizeSpan( + findOperation(events, "cursor-sdk-resume-conversation-operation"), + ), + task: summarizeSpan(conversationTask), + }, + prompt: { + operation: summarizeSpan( + findOperation(events, "cursor-sdk-prompt-operation"), + ), + task: summarizeSpan(promptTask), + }, + root: summarizeSpan(findLatestSpan(events, ROOT_NAME)), + stream: { + operation: summarizeSpan( + findOperation(events, "cursor-sdk-stream-operation"), + ), + subagent_task: summarizeSpan(subagentTask), + task: summarizeSpan(streamTask), + tool: summarizeSpan(tool), + }, + wait: { + operation: summarizeSpan( + findOperation(events, "cursor-sdk-wait-operation"), + ), + task: summarizeSpan(waitTask), + }, + } as Json); +} + +export function defineCursorSDKInstrumentationAssertions(options: { + name: string; + runScenario: RunCursorSDKScenario; + snapshotName: string; + testFileUrl: string; + timeoutMs: number; +}): void { + const snapshotPath = resolveFileSnapshotPath( + options.testFileUrl, + `${options.snapshotName}.span-events.json`, + ); + const testConfig = { timeout: options.timeoutMs }; + + describe(options.name, () => { + let events: CapturedLogEvent[] = []; + + beforeAll(async () => { + await withScenarioHarness(async (harness) => { + await options.runScenario(harness); + events = harness.events(); + }); + }, options.timeoutMs); + + test("captures the root trace", testConfig, () => { + const root = findLatestSpan(events, ROOT_NAME); + + expect(root).toBeDefined(); + expect(root?.row.metadata).toMatchObject({ scenario: SCENARIO_NAME }); + }); + + test( + "captures Cursor Agent task spans for run-producing APIs", + testConfig, + () => { + for (const operationName of [ + "cursor-sdk-prompt-operation", + "cursor-sdk-stream-operation", + "cursor-sdk-wait-operation", + "cursor-sdk-resume-conversation-operation", + ]) { + const operation = findOperation(events, operationName); + const task = findCursorTask(events, operationName); + + expect(operation).toBeDefined(); + expect(task).toBeDefined(); + expect(task?.span.parentIds).toEqual([operation?.span.id ?? ""]); + expect(task?.row.metadata).toMatchObject({ + provider: "cursor", + }); + } + }, + ); + + test( + "captures tool spans when Cursor surfaces tool calls", + testConfig, + () => { + const streamTask = findCursorTask( + events, + "cursor-sdk-stream-operation", + ); + const toolSpans = events.filter( + (event) => + event.span.type === "tool" && + event.span.parentIds.includes(streamTask?.span.id ?? ""), + ); + + expect(toolSpans.length).toBeGreaterThan(0); + expect( + toolSpans.some( + (event) => + event.input !== undefined && + event.output !== undefined && + event.metadata?.["cursor_sdk.tool.status"] === "completed", + ), + ).toBe(true); + expect( + JSON.stringify(toolSpans.map((event) => event.output)), + ).toContain("cursor_tool_ok"); + }, + ); + + test("preserves user onDelta/onStep callbacks", testConfig, () => { + expect(findLatestSpan(events, "cursor-sdk-user-on-delta")).toBeDefined(); + expect(findLatestSpan(events, "cursor-sdk-user-on-step")).toBeDefined(); + + const waitTask = findCursorTask(events, "cursor-sdk-wait-operation"); + expect(waitTask?.metrics).toMatchObject({ + completion_tokens: expect.any(Number), + prompt_tokens: expect.any(Number), + }); + expect(waitTask?.metrics?.["cursor_sdk.step_duration_ms"]).toEqual( + expect.any(Number), + ); + expect(outputText(waitTask)).toContain("CURSOR_WAIT_OK"); + }); + + test("captures conversation output text", testConfig, () => { + const conversationTask = findCursorTask( + events, + "cursor-sdk-resume-conversation-operation", + ); + + expect(outputText(conversationTask)).toContain("CURSOR_CONVERSATION_OK"); + }); + + test("matches the shared span snapshot", testConfig, async () => { + await expect( + formatJsonFileSnapshot(summarize(events)), + ).toMatchFileSnapshot(snapshotPath); + }); + }); +} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/package.json b/e2e/scenarios/cursor-sdk-instrumentation/package.json new file mode 100644 index 000000000..4cf9f2128 --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/package.json @@ -0,0 +1,20 @@ +{ + "name": "@braintrust/e2e-cursor-sdk-instrumentation", + "private": true, + "braintrustScenario": { + "canary": { + "dependencies": { + "cursor-sdk-v1": "@cursor/sdk@latest" + } + } + }, + "dependencies": { + "cursor-sdk-v1": "npm:@cursor/sdk@1.0.10", + "dotenv": "^17.2.3" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sqlite3" + ] + } +} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml b/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml new file mode 100644 index 000000000..ce4bdb412 --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml @@ -0,0 +1,1168 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + cursor-sdk-v1: + specifier: npm:@cursor/sdk@1.0.10 + version: '@cursor/sdk@1.0.10' + dotenv: + specifier: ^17.2.3 + version: 17.4.2 + +packages: + + '@bufbuild/protobuf@1.10.0': + resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + + '@connectrpc/connect-node@1.7.0': + resolution: {integrity: sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@bufbuild/protobuf': ^1.10.0 + '@connectrpc/connect': 1.7.0 + + '@connectrpc/connect@1.7.0': + resolution: {integrity: sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==} + peerDependencies: + '@bufbuild/protobuf': ^1.10.0 + + '@cursor/sdk-darwin-arm64@1.0.10': + resolution: {integrity: sha512-uwNhyH2fyJhiSHhgWlozeuelBMyjotVN7jmqrPxaBR2Qii4JYmuhlNvo4fiNhojvLjC5EMF1pnM5tr+Uyt/G1g==} + cpu: [arm64] + os: [darwin] + + '@cursor/sdk-darwin-x64@1.0.10': + resolution: {integrity: sha512-Z0IVJB5cfyQ3lHz9MEjyH8bnmpaLRx/eh1E6MKC95lLr5K+1jPITsKgK3P9NwhIl1kc0NEA/z90mXxDOWoc2fg==} + cpu: [x64] + os: [darwin] + + '@cursor/sdk-linux-arm64@1.0.10': + resolution: {integrity: sha512-443sB9wDmlsdMDSgcGbmaNf5H+3IoIFhnmxSACFXbdFYNYj4U6e1TWxJqpl/FI/MjTodQFNQBvGbUo6SUcwj8w==} + cpu: [arm64] + os: [linux] + + '@cursor/sdk-linux-x64@1.0.10': + resolution: {integrity: sha512-elRt/lsH6xw1LyD4HcPAJINk5q7Apj4F68lmemb0UZOC01w5PfHsjUkURg7CkPWL7PmNgUjxXTaQe3EdEq8now==} + cpu: [x64] + os: [linux] + + '@cursor/sdk-win32-x64@1.0.10': + resolution: {integrity: sha512-5Fyb7aZYnSPRQPg/reHpwEw8SDhJHg1W+ARyDCByysI2II59RFqqBdlDay7iwUCKaziemuebFK5KNSVt8WlYTA==} + cpu: [x64] + os: [win32] + + '@cursor/sdk@1.0.10': + resolution: {integrity: sha512-j2y2sbDBgxMPZqXWUyCRfzatpD4h0Vg4SLvVLBV+j65A8m+e9gTdrSUK3eaUdIs9IAAZe1gngP2aOKMw6/tq+Q==} + engines: {node: '>=18'} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@statsig/client-core@3.31.0': + resolution: {integrity: sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==} + + '@statsig/js-client@3.31.0': + resolution: {integrity: sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@10.1.1: + resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} + engines: {node: '>= 12'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.8: + resolution: {integrity: sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@bufbuild/protobuf@1.10.0': {} + + '@connectrpc/connect-node@1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0))': + dependencies: + '@bufbuild/protobuf': 1.10.0 + '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) + undici: 5.29.0 + + '@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0)': + dependencies: + '@bufbuild/protobuf': 1.10.0 + + '@cursor/sdk-darwin-arm64@1.0.10': + optional: true + + '@cursor/sdk-darwin-x64@1.0.10': + optional: true + + '@cursor/sdk-linux-arm64@1.0.10': + optional: true + + '@cursor/sdk-linux-x64@1.0.10': + optional: true + + '@cursor/sdk-win32-x64@1.0.10': + optional: true + + '@cursor/sdk@1.0.10': + dependencies: + '@bufbuild/protobuf': 1.10.0 + '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) + '@connectrpc/connect-node': 1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0)) + '@statsig/js-client': 3.31.0 + sqlite3: 5.1.7 + zod: 3.25.76 + optionalDependencies: + '@cursor/sdk-darwin-arm64': 1.0.10 + '@cursor/sdk-darwin-x64': 1.0.10 + '@cursor/sdk-linux-arm64': 1.0.10 + '@cursor/sdk-linux-x64': 1.0.10 + '@cursor/sdk-win32-x64': 1.0.10 + transitivePeerDependencies: + - bluebird + - supports-color + + '@fastify/busboy@2.1.1': {} + + '@gar/promisify@1.1.3': + optional: true + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@statsig/client-core@3.31.0': {} + + '@statsig/js-client@3.31.0': + dependencies: + '@statsig/client-core': 3.31.0 + + '@tootallnate/once@1.1.2': + optional: true + + abbrev@1.1.1: + optional: true + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ansi-regex@5.0.1: + optional: true + + aproba@2.1.0: + optional: true + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + balanced-match@1.0.2: + optional: true + + base64-js@1.5.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + clean-stack@2.2.0: + optional: true + + color-support@1.1.3: + optional: true + + concat-map@0.0.1: + optional: true + + console-control-strings@1.1.0: + optional: true + + debug@4.4.3: + dependencies: + ms: 2.1.3 + optional: true + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + delegates@1.0.0: + optional: true + + detect-libc@2.1.2: {} + + dotenv@17.4.2: {} + + emoji-regex@8.0.0: + optional: true + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@2.2.1: + optional: true + + err-code@2.0.3: + optional: true + + expand-template@2.0.3: {} + + file-uri-to-path@1.0.0: {} + + fs-constants@1.0.0: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: + optional: true + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + github-from-package@0.0.0: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + graceful-fs@4.2.11: + optional: true + + has-unicode@2.0.1: + optional: true + + http-cache-semantics@4.2.0: + optional: true + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ip-address@10.1.1: + optional: true + + is-fullwidth-code-point@3.0.0: + optional: true + + is-lambda@1.0.1: + optional: true + + isexe@2.0.0: + optional: true + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + mimic-response@3.1.0: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + optional: true + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@1.0.4: {} + + ms@2.1.3: + optional: true + + napi-build-utils@2.0.0: {} + + negotiator@0.6.4: + optional: true + + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + + node-addon-api@7.1.1: {} + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + path-is-absolute@1.0.1: + optional: true + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.89.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + promise-inflight@1.0.1: + optional: true + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + retry@0.12.0: + optional: true + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: + optional: true + + semver@7.7.4: {} + + set-blocking@2.0.0: + optional: true + + signal-exit@3.0.7: + optional: true + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + smart-buffer@4.2.0: + optional: true + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.8 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.8: + dependencies: + ip-address: 10.1.1 + smart-buffer: 4.2.0 + optional: true + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + optional: true + + strip-json-comments@2.0.1: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + + util-deprecate@1.0.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + optional: true + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + wrappy@1.0.2: {} + + yallist@4.0.0: {} + + zod@3.25.76: {} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.mjs b/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.mjs new file mode 100644 index 000000000..d7df96baa --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.mjs @@ -0,0 +1,5 @@ +import * as cursorSDK from "cursor-sdk-v1"; +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runAutoCursorSDKInstrumentation } from "./scenario.impl.mjs"; + +runMain(async () => runAutoCursorSDKInstrumentation(cursorSDK)); diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.ts b/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.ts new file mode 100644 index 000000000..4fb25222d --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.cursor-sdk-v1.ts @@ -0,0 +1,5 @@ +import * as cursorSDK from "cursor-sdk-v1"; +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runWrappedCursorSDKInstrumentation } from "./scenario.impl.mjs"; + +runMain(async () => runWrappedCursorSDKInstrumentation(cursorSDK)); diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs new file mode 100644 index 000000000..1270184e4 --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs @@ -0,0 +1,143 @@ +import { config as loadDotEnv } from "dotenv"; +import { resolve } from "node:path"; +import { traced, wrapCursorSDK } from "braintrust"; +import { + collectAsync, + runOperation, + runTracedScenario, +} from "../../helpers/provider-runtime.mjs"; + +loadDotEnv({ + path: resolve(process.env.BRAINTRUST_E2E_REPO_ROOT ?? process.cwd(), ".env"), + quiet: true, +}); + +const CURSOR_MODEL = "composer-2"; + +export const ROOT_NAME = "cursor-sdk-root"; +export const SCENARIO_NAME = "cursor-sdk-instrumentation"; + +function cursorOptions() { + return { + apiKey: process.env.CURSOR_API_KEY, + local: { + cwd: process.cwd(), + sandboxOptions: { enabled: false }, + }, + model: { id: CURSOR_MODEL }, + }; +} + +async function disposeAgent(agent) { + if (agent?.[Symbol.asyncDispose]) { + await agent[Symbol.asyncDispose](); + } else if (agent?.close) { + agent.close(); + } +} + +async function runCursorSDKScenario({ decorateSDK, sdk }) { + if (!process.env.CURSOR_API_KEY) { + throw new Error( + "CURSOR_API_KEY is required for cursor-sdk-instrumentation", + ); + } + + const instrumentedSDK = decorateSDK ? decorateSDK(sdk) : sdk; + const { Agent } = instrumentedSDK; + let reusableAgent; + + await runTracedScenario({ + callback: async () => { + await runOperation("cursor-sdk-prompt-operation", "prompt", async () => { + await Agent.prompt( + "Reply with exactly: CURSOR_PROMPT_OK. Do not modify files.", + cursorOptions(), + ); + }); + + await runOperation("cursor-sdk-stream-operation", "stream", async () => { + reusableAgent = await Agent.create({ + ...cursorOptions(), + agents: { + reviewer: { + description: "Reads the request and replies briefly.", + model: "inherit", + prompt: "Reply concisely. Do not modify files.", + }, + }, + }); + const run = await reusableAgent.send( + "Run the shell command `printf cursor_tool_ok` and report the output. Do not edit files.", + ); + await collectAsync(run.stream()); + }); + + await runOperation("cursor-sdk-wait-operation", "wait", async () => { + const agent = await Agent.create(cursorOptions()); + try { + const run = await agent.send( + "Reply with exactly: CURSOR_WAIT_OK. Do not modify files.", + { + onDelta: async ({ update }) => { + await traced(async () => update.type, { + name: "cursor-sdk-user-on-delta", + }); + }, + onStep: async ({ step }) => { + await traced(async () => step.type, { + name: "cursor-sdk-user-on-step", + }); + }, + }, + ); + await run.wait(); + } finally { + await disposeAgent(agent); + } + }); + + await runOperation( + "cursor-sdk-resume-conversation-operation", + "resume-conversation", + async () => { + const agentId = reusableAgent?.agentId; + if (!agentId) { + throw new Error("Expected reusable Cursor agent id"); + } + const agent = await Agent.resume(agentId, cursorOptions()); + try { + const run = await agent.send( + "Reply with exactly: CURSOR_CONVERSATION_OK. Do not modify files.", + ); + await run.conversation(); + } finally { + await disposeAgent(agent); + } + }, + ); + }, + flushCount: 2, + flushDelayMs: 250, + metadata: { + scenario: SCENARIO_NAME, + }, + projectNameBase: "e2e-cursor-sdk-instrumentation", + rootName: ROOT_NAME, + }); + + await disposeAgent(reusableAgent); +} + +export async function runWrappedCursorSDKInstrumentation(sdk) { + await runCursorSDKScenario({ + decorateSDK: wrapCursorSDK, + sdk, + }); +} + +export async function runAutoCursorSDKInstrumentation(sdk) { + await runCursorSDKScenario({ + sdk, + }); +} diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts new file mode 100644 index 000000000..6b5d497d9 --- /dev/null +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.test.ts @@ -0,0 +1,56 @@ +import { describe } from "vitest"; +import { + prepareScenarioDir, + readInstalledPackageVersion, + resolveScenarioDir, +} from "../../helpers/scenario-harness"; +import { defineCursorSDKInstrumentationAssertions } from "./assertions"; + +const scenarioDir = await prepareScenarioDir({ + scenarioDir: resolveScenarioDir(import.meta.url), +}); +const TIMEOUT_MS = 240_000; +const cursorSDKScenario = { + autoEntry: "scenario.cursor-sdk-v1.mjs", + autoSnapshotName: "cursor-sdk-v1-auto-hook", + dependencyName: "cursor-sdk-v1", + version: await readInstalledPackageVersion(scenarioDir, "cursor-sdk-v1"), + wrapperEntry: "scenario.cursor-sdk-v1.ts", + wrapperSnapshotName: "cursor-sdk-v1-wrapped", + variantKey: "cursor-sdk-v1", +}; + +describe("wrapped instrumentation", () => { + defineCursorSDKInstrumentationAssertions({ + name: `cursor sdk ${cursorSDKScenario.version}`, + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: cursorSDKScenario.wrapperEntry, + runContext: { variantKey: cursorSDKScenario.variantKey }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: cursorSDKScenario.wrapperSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); +}); + +describe("auto-hook instrumentation", () => { + defineCursorSDKInstrumentationAssertions({ + name: `cursor sdk ${cursorSDKScenario.version}`, + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: cursorSDKScenario.autoEntry, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { variantKey: cursorSDKScenario.variantKey }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: cursorSDKScenario.autoSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); +}); diff --git a/e2e/scripts/run-canary-tests-docker.mjs b/e2e/scripts/run-canary-tests-docker.mjs index 0635ba044..9b75bb175 100644 --- a/e2e/scripts/run-canary-tests-docker.mjs +++ b/e2e/scripts/run-canary-tests-docker.mjs @@ -19,6 +19,7 @@ const ALLOWED_ENV_KEYS = [ "GEMINI_API_KEY", "GOOGLE_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 3202d3dbc..2462c5faf 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -24,6 +24,7 @@ import { openaiConfigs } from "../configs/openai"; import { anthropicConfigs } from "../configs/anthropic"; import { aiSDKConfigs } from "../configs/ai-sdk"; import { claudeAgentSDKConfigs } from "../configs/claude-agent-sdk"; +import { cursorSDKConfigs } from "../configs/cursor-sdk"; import { googleGenAIConfigs } from "../configs/google-genai"; import { huggingFaceConfigs } from "../configs/huggingface"; import { openRouterAgentConfigs } from "../configs/openrouter-agent"; @@ -76,6 +77,7 @@ export const unplugin = createUnplugin((options = {}) => { ...anthropicConfigs, ...aiSDKConfigs, ...claudeAgentSDKConfigs, + ...cursorSDKConfigs, ...googleGenAIConfigs, ...huggingFaceConfigs, ...openRouterConfigs, diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 5d6c7e20b..966c659aa 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -33,6 +33,7 @@ import { openaiConfigs } from "../configs/openai"; import { anthropicConfigs } from "../configs/anthropic"; import { aiSDKConfigs } from "../configs/ai-sdk"; import { claudeAgentSDKConfigs } from "../configs/claude-agent-sdk"; +import { cursorSDKConfigs } from "../configs/cursor-sdk"; import { googleGenAIConfigs } from "../configs/google-genai"; import { huggingFaceConfigs } from "../configs/huggingface"; import { openRouterAgentConfigs } from "../configs/openrouter-agent"; @@ -71,6 +72,7 @@ function getMatcher(options: BundlerPluginOptions): InstrumentationMatcher { ...anthropicConfigs, ...aiSDKConfigs, ...claudeAgentSDKConfigs, + ...cursorSDKConfigs, ...googleGenAIConfigs, ...huggingFaceConfigs, ...openRouterConfigs, diff --git a/js/src/auto-instrumentations/configs/cursor-sdk.ts b/js/src/auto-instrumentations/configs/cursor-sdk.ts new file mode 100644 index 000000000..4aa8ed631 --- /dev/null +++ b/js/src/auto-instrumentations/configs/cursor-sdk.ts @@ -0,0 +1,49 @@ +import type { InstrumentationConfig } from "@apm-js-collab/code-transformer"; +import { cursorSDKChannels } from "../../instrumentation/plugins/cursor-sdk-channels"; + +const cursorSDKVersionRange = ">=1.0.7 <2.0.0"; + +const cursorSDKEntrypoints = ["dist/esm/index.js", "dist/cjs/index.js"]; + +export const cursorSDKConfigs: InstrumentationConfig[] = + cursorSDKEntrypoints.flatMap((filePath) => [ + { + channelName: cursorSDKChannels.create.channelName, + module: { + name: "@cursor/sdk", + versionRange: cursorSDKVersionRange, + filePath, + }, + functionQuery: { + className: "Agent", + methodName: "create", + kind: "Async", + }, + }, + { + channelName: cursorSDKChannels.resume.channelName, + module: { + name: "@cursor/sdk", + versionRange: cursorSDKVersionRange, + filePath, + }, + functionQuery: { + className: "Agent", + methodName: "resume", + kind: "Async", + }, + }, + { + channelName: cursorSDKChannels.prompt.channelName, + module: { + name: "@cursor/sdk", + versionRange: cursorSDKVersionRange, + filePath, + }, + functionQuery: { + className: "Agent", + methodName: "prompt", + kind: "Async", + }, + }, + ]); diff --git a/js/src/auto-instrumentations/hook.mts b/js/src/auto-instrumentations/hook.mts index 2c3622600..46fe0f0d2 100644 --- a/js/src/auto-instrumentations/hook.mts +++ b/js/src/auto-instrumentations/hook.mts @@ -18,6 +18,7 @@ import { openaiConfigs } from "./configs/openai.js"; import { anthropicConfigs } from "./configs/anthropic.js"; import { aiSDKConfigs } from "./configs/ai-sdk.js"; import { claudeAgentSDKConfigs } from "./configs/claude-agent-sdk.js"; +import { cursorSDKConfigs } from "./configs/cursor-sdk.js"; import { googleGenAIConfigs } from "./configs/google-genai.js"; import { huggingFaceConfigs } from "./configs/huggingface.js"; import { openRouterAgentConfigs } from "./configs/openrouter-agent.js"; @@ -68,6 +69,9 @@ const allConfigs = [ ...(isDisabled(disabledIntegrations, "claudeagentsdk", "claude-agent-sdk") ? [] : claudeAgentSDKConfigs), + ...(isDisabled(disabledIntegrations, "cursor", "cursor-sdk") + ? [] + : cursorSDKConfigs), ...(isDisabled(disabledIntegrations, "google", "google-genai") ? [] : googleGenAIConfigs), diff --git a/js/src/auto-instrumentations/index.ts b/js/src/auto-instrumentations/index.ts index 03fda75a1..bdac954ab 100644 --- a/js/src/auto-instrumentations/index.ts +++ b/js/src/auto-instrumentations/index.ts @@ -32,6 +32,7 @@ export { openaiConfigs } from "./configs/openai"; export { anthropicConfigs } from "./configs/anthropic"; export { aiSDKConfigs } from "./configs/ai-sdk"; export { claudeAgentSDKConfigs } from "./configs/claude-agent-sdk"; +export { cursorSDKConfigs } from "./configs/cursor-sdk"; export { googleGenAIConfigs } from "./configs/google-genai"; export { huggingFaceConfigs } from "./configs/huggingface"; export { openRouterAgentConfigs } from "./configs/openrouter-agent"; diff --git a/js/src/exports.ts b/js/src/exports.ts index 93a12bb05..02dadbf66 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -177,6 +177,7 @@ export { export { wrapAnthropic } from "./wrappers/anthropic"; export { wrapMastraAgent } from "./wrappers/mastra"; export { wrapClaudeAgentSDK } from "./wrappers/claude-agent-sdk/claude-agent-sdk"; +export { wrapCursorSDK } from "./wrappers/cursor-sdk"; export { wrapGoogleGenAI } from "./wrappers/google-genai"; export { wrapGoogleADK } from "./wrappers/google-adk"; export { wrapHuggingFace } from "./wrappers/huggingface"; diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts index 485d2d9bc..5db01b441 100644 --- a/js/src/instrumentation/braintrust-plugin.ts +++ b/js/src/instrumentation/braintrust-plugin.ts @@ -3,6 +3,7 @@ import { OpenAIPlugin } from "./plugins/openai-plugin"; import { AnthropicPlugin } from "./plugins/anthropic-plugin"; import { AISDKPlugin } from "./plugins/ai-sdk-plugin"; import { ClaudeAgentSDKPlugin } from "./plugins/claude-agent-sdk-plugin"; +import { CursorSDKPlugin } from "./plugins/cursor-sdk-plugin"; import { GoogleGenAIPlugin } from "./plugins/google-genai-plugin"; import { HuggingFacePlugin } from "./plugins/huggingface-plugin"; import { OpenRouterAgentPlugin } from "./plugins/openrouter-agent-plugin"; @@ -22,6 +23,8 @@ export interface BraintrustPluginConfig { googleGenAI?: boolean; huggingface?: boolean; claudeAgentSDK?: boolean; + cursor?: boolean; + cursorSDK?: boolean; openrouter?: boolean; openrouterAgent?: boolean; mistral?: boolean; @@ -53,6 +56,7 @@ export class BraintrustPlugin extends BasePlugin { private anthropicPlugin: AnthropicPlugin | null = null; private aiSDKPlugin: AISDKPlugin | null = null; private claudeAgentSDKPlugin: ClaudeAgentSDKPlugin | null = null; + private cursorSDKPlugin: CursorSDKPlugin | null = null; private googleGenAIPlugin: GoogleGenAIPlugin | null = null; private huggingFacePlugin: HuggingFacePlugin | null = null; private openRouterPlugin: OpenRouterPlugin | null = null; @@ -95,6 +99,11 @@ export class BraintrustPlugin extends BasePlugin { this.claudeAgentSDKPlugin.enable(); } + if (integrations.cursorSDK !== false && integrations.cursor !== false) { + this.cursorSDKPlugin = new CursorSDKPlugin(); + this.cursorSDKPlugin.enable(); + } + // Enable Google GenAI integration (default: true) // Support both 'googleGenAI' and legacy 'google' config keys if (integrations.googleGenAI !== false && integrations.google !== false) { @@ -160,6 +169,11 @@ export class BraintrustPlugin extends BasePlugin { this.claudeAgentSDKPlugin = null; } + if (this.cursorSDKPlugin) { + this.cursorSDKPlugin.disable(); + this.cursorSDKPlugin = null; + } + if (this.googleGenAIPlugin) { this.googleGenAIPlugin.disable(); this.googleGenAIPlugin = null; diff --git a/js/src/instrumentation/plugins/cursor-sdk-channels.ts b/js/src/instrumentation/plugins/cursor-sdk-channels.ts new file mode 100644 index 000000000..b38f7246a --- /dev/null +++ b/js/src/instrumentation/plugins/cursor-sdk-channels.ts @@ -0,0 +1,47 @@ +import { channel, defineChannels } from "../core/channel-definitions"; +import type { + CursorSDKAgent, + CursorSDKAgentOptions, + CursorSDKRun, + CursorSDKRunResult, + CursorSDKSendOptions, + CursorSDKUserMessage, +} from "../../vendor-sdk-types/cursor-sdk"; + +export const cursorSDKChannels = defineChannels("@cursor/sdk", { + create: channel< + [CursorSDKAgentOptions], + CursorSDKAgent, + Record + >({ + channelName: "Agent.create", + kind: "async", + }), + resume: channel< + [string, Partial | undefined], + CursorSDKAgent, + Record + >({ + channelName: "Agent.resume", + kind: "async", + }), + prompt: channel< + [string | CursorSDKUserMessage, CursorSDKAgentOptions | undefined], + CursorSDKRunResult, + Record + >({ + channelName: "Agent.prompt", + kind: "async", + }), + send: channel< + [string | CursorSDKUserMessage, CursorSDKSendOptions | undefined], + CursorSDKRun, + { + agent?: CursorSDKAgent; + operation?: "send"; + } + >({ + channelName: "agent.send", + kind: "async", + }), +}); diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts b/js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts new file mode 100644 index 000000000..909fceb02 --- /dev/null +++ b/js/src/instrumentation/plugins/cursor-sdk-plugin.test.ts @@ -0,0 +1,259 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockStartSpan } = vi.hoisted(() => ({ + mockStartSpan: vi.fn(), +})); + +vi.mock("../../isomorph", () => ({ + default: { + newTracingChannel: vi.fn(), + }, +})); + +vi.mock("../../logger", () => ({ + startSpan: (...args: unknown[]) => mockStartSpan(...args), +})); + +import iso from "../../isomorph"; +import { CursorSDKPlugin } from "./cursor-sdk-plugin"; + +const mockNewTracingChannel = iso.newTracingChannel as ReturnType; + +describe("CursorSDKPlugin", () => { + let handlersByName: Map; + let spans: Array<{ + end: ReturnType; + export: ReturnType; + log: ReturnType; + name?: string; + }>; + + beforeEach(() => { + handlersByName = new Map(); + spans = []; + mockNewTracingChannel.mockImplementation((name: string) => ({ + subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), + tracePromise: vi.fn((fn) => fn()), + unsubscribe: vi.fn(), + })); + mockStartSpan.mockImplementation((args: any) => { + const span = { + end: vi.fn(), + export: vi.fn(async () => `${args.name}-export-${spans.length}`), + log: vi.fn(), + name: args.name, + }; + if (args.event) { + span.log(args.event); + } + spans.push(span); + return span; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("subscribes to Cursor SDK channels", () => { + const plugin = new CursorSDKPlugin(); + + plugin.enable(); + + expect(handlersByName.has("orchestrion:@cursor/sdk:Agent.create")).toBe( + true, + ); + expect(handlersByName.has("orchestrion:@cursor/sdk:Agent.resume")).toBe( + true, + ); + expect(handlersByName.has("orchestrion:@cursor/sdk:Agent.prompt")).toBe( + true, + ); + expect(handlersByName.has("orchestrion:@cursor/sdk:agent.send")).toBe(true); + }); + + it("patches agents returned by Agent.create and traces send/wait", async () => { + const plugin = new CursorSDKPlugin(); + plugin.enable(); + + const createHandlers = handlersByName.get( + "orchestrion:@cursor/sdk:Agent.create", + ); + const sendHandlers = handlersByName.get( + "orchestrion:@cursor/sdk:agent.send", + ); + const run = makeRun(); + const originalSend = vi.fn(async () => run); + const agent = { + agentId: "agent-1", + send: originalSend, + }; + + createHandlers.asyncEnd({ + arguments: [{ local: { cwd: "/tmp/repo" } }], + result: agent, + }); + + const patchedRun = await agent.send("use a tool", {}); + expect(patchedRun).toBe(run); + expect(originalSend).toHaveBeenCalledTimes(1); + + const sendEvent = { + agent, + arguments: ["use a tool", {}], + result: run, + }; + sendHandlers.start(sendEvent); + sendHandlers.asyncEnd(sendEvent); + + await run.wait(); + + const rootSpan = spans.find((span) => span.name === "Cursor Agent"); + expect(rootSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + input: "use a tool", + metadata: expect.objectContaining({ + "cursor_sdk.agent_id": "agent-1", + provider: "cursor", + }), + }), + ); + expect(rootSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + output: "done", + metadata: expect.objectContaining({ + "cursor_sdk.run_id": "run-1", + "cursor_sdk.status": "finished", + }), + }), + ); + expect(rootSpan?.end).toHaveBeenCalledTimes(1); + }); + + it("captures stream tool calls and usage", async () => { + const plugin = new CursorSDKPlugin(); + plugin.enable(); + + const sendHandlers = handlersByName.get( + "orchestrion:@cursor/sdk:agent.send", + ); + const run = makeRun([ + { + type: "tool_call", + call_id: "call-1", + name: "shell", + status: "running", + args: { command: "echo hi" }, + }, + { + type: "tool_call", + call_id: "call-1", + name: "shell", + status: "completed", + result: { stdout: "hi\n" }, + }, + { + type: "assistant", + message: { content: [{ type: "text", text: "done" }] }, + }, + ]); + const event = { + agent: { agentId: "agent-1" }, + arguments: [ + "hello", + { + onDelta: vi.fn(), + }, + ], + result: run, + }; + + sendHandlers.start(event); + await event.arguments[1].onDelta({ + update: { + type: "turn-ended", + usage: { + inputTokens: 3, + outputTokens: 4, + cacheReadTokens: 1, + cacheWriteTokens: 2, + }, + }, + }); + sendHandlers.asyncEnd(event); + + const chunks = []; + for await (const chunk of run.stream()) { + chunks.push(chunk); + } + + expect(chunks).toHaveLength(3); + const toolSpan = spans.find((span) => span.name === "tool: shell"); + expect(toolSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + input: { command: "echo hi" }, + }), + ); + expect(toolSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + output: { stdout: "hi\n" }, + }), + ); + const rootSpan = spans.find((span) => span.name === "Cursor Agent"); + expect(rootSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + metrics: expect.objectContaining({ + completion_tokens: 4, + prompt_cache_creation_tokens: 2, + prompt_cached_tokens: 1, + prompt_tokens: 3, + }), + output: "done", + }), + ); + }); + + it("traces Agent.prompt without a nested send span", () => { + const plugin = new CursorSDKPlugin(); + plugin.enable(); + + const promptHandlers = handlersByName.get( + "orchestrion:@cursor/sdk:Agent.prompt", + ); + const sendHandlers = handlersByName.get( + "orchestrion:@cursor/sdk:agent.send", + ); + const promptEvent = { arguments: ["hello", { local: { cwd: "/tmp" } }] }; + + promptHandlers.start(promptEvent); + sendHandlers.start({ arguments: ["nested", {}] }); + promptHandlers.asyncEnd({ + ...promptEvent, + result: { id: "run-1", result: "done", status: "finished" }, + }); + + expect(spans.filter((span) => span.name === "Cursor Agent")).toHaveLength( + 1, + ); + }); +}); + +function makeRun(messages: unknown[] = []) { + return { + agentId: "agent-1", + async conversation() { + return []; + }, + id: "run-1", + result: "done", + status: "finished", + stream: async function* () { + for (const message of messages) { + yield message; + } + }, + async wait() { + return { id: "run-1", result: "done", status: "finished" }; + }, + }; +} diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts new file mode 100644 index 000000000..1788e4f6e --- /dev/null +++ b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts @@ -0,0 +1,1179 @@ +import { BasePlugin } from "../core"; +import type { ChannelMessage } from "../core/channel-definitions"; +import type { IsoChannelHandlers } from "../../isomorph"; +import { startSpan } from "../../logger"; +import type { Span } from "../../logger"; +import { getCurrentUnixTimestamp } from "../../util"; +import { SpanTypeAttribute } from "../../../util/index"; +import { cursorSDKChannels } from "./cursor-sdk-channels"; +import type { + CursorSDKAgent, + CursorSDKAgentOptions, + CursorSDKConversationStep, + CursorSDKConversationTurn, + CursorSDKInteractionUpdate, + CursorSDKMessage, + CursorSDKModelSelection, + CursorSDKRun, + CursorSDKRunGitInfo, + CursorSDKRunResult, + CursorSDKSendOptions, + CursorSDKToolCall, + CursorSDKToolUseMessage, + CursorSDKUsage, + CursorSDKUserMessage, +} from "../../vendor-sdk-types/cursor-sdk"; + +const PATCHED_AGENT = Symbol.for("braintrust.cursor-sdk.auto-patched-agent"); +const PATCHED_RUN = Symbol.for("braintrust.cursor-sdk.patched-run"); +const WRAPPED_AGENT = Symbol.for("braintrust.cursor-sdk.wrapped-agent"); + +type ToolState = { + span: Span; + subAgentSpan?: Span; +}; + +type CursorRunState = { + activeToolSpans: Map; + agent?: CursorSDKAgent; + deltaText: string[]; + streamText: string[]; + stepText: string[]; + conversationOutput?: unknown; + conversationText: string[]; + finalized: boolean; + input: string | CursorSDKUserMessage; + lastResult?: CursorSDKRunResult; + metadata: Record; + metrics: Record; + run?: CursorSDKRun; + span: Span; + startTime: number; + streamMessages: CursorSDKMessage[]; + taskText: string[]; +}; + +type PromptState = { + metadata: Record; + span: Span; + startTime: number; +}; + +export class CursorSDKPlugin extends BasePlugin { + private promptDepth = 0; + + protected onEnable(): void { + this.subscribeToAgentFactories(); + this.subscribeToPrompt(); + this.subscribeToSend(); + } + + protected onDisable(): void { + for (const unsubscribe of this.unsubscribers) { + unsubscribe(); + } + this.unsubscribers = []; + this.promptDepth = 0; + } + + private subscribeToAgentFactories(): void { + this.subscribeToAgentFactory(cursorSDKChannels.create); + this.subscribeToAgentFactory(cursorSDKChannels.resume); + } + + private subscribeToAgentFactory( + channel: typeof cursorSDKChannels.create | typeof cursorSDKChannels.resume, + ): void { + const tracingChannel = channel.tracingChannel(); + const handlers: IsoChannelHandlers> = { + asyncEnd: (event) => { + patchCursorAgentInPlace(event.result); + }, + error: () => {}, + }; + + tracingChannel.subscribe(handlers); + this.unsubscribers.push(() => { + tracingChannel.unsubscribe(handlers); + }); + } + + private subscribeToPrompt(): void { + const channel = cursorSDKChannels.prompt.tracingChannel(); + const states = new WeakMap(); + + const handlers: IsoChannelHandlers< + ChannelMessage + > = { + start: (event) => { + this.promptDepth += 1; + const message = event.arguments[0]; + const options = event.arguments[1]; + const metadata = { + ...extractAgentOptionsMetadata(options), + "cursor_sdk.operation": "Agent.prompt", + provider: "cursor", + ...(event.moduleVersion + ? { "cursor_sdk.version": event.moduleVersion } + : {}), + }; + const span = startSpan({ + name: "Cursor Agent", + spanAttributes: { type: SpanTypeAttribute.TASK }, + }); + const startTime = getCurrentUnixTimestamp(); + safeLog(span, { + input: sanitizeUserMessage(message), + metadata, + }); + states.set(event, { metadata, span, startTime }); + }, + asyncEnd: (event) => { + this.promptDepth = Math.max(0, this.promptDepth - 1); + const state = states.get(event); + if (!state) { + return; + } + try { + safeLog(state.span, { + metadata: { + ...state.metadata, + ...extractRunResultMetadata(event.result), + }, + metrics: buildDurationMetrics(state.startTime), + output: event.result?.result ?? event.result, + }); + } finally { + state.span.end(); + states.delete(event); + } + }, + error: (event) => { + this.promptDepth = Math.max(0, this.promptDepth - 1); + const state = states.get(event); + if (!state || !event.error) { + return; + } + safeLog(state.span, { error: event.error.message }); + state.span.end(); + states.delete(event); + }, + }; + + channel.subscribe(handlers); + this.unsubscribers.push(() => { + channel.unsubscribe(handlers); + }); + } + + private subscribeToSend(): void { + const channel = cursorSDKChannels.send.tracingChannel(); + const states = new WeakMap(); + + const handlers: IsoChannelHandlers< + ChannelMessage + > = { + start: (event) => { + if (this.promptDepth > 0) { + return; + } + + const message = event.arguments[0]; + const sendOptions = event.arguments[1]; + const agent = event.agent; + const metadata = { + ...extractSendMetadata(sendOptions), + ...(agent ? extractAgentMetadata(agent) : {}), + "cursor_sdk.operation": "agent.send", + provider: "cursor", + ...(event.moduleVersion + ? { "cursor_sdk.version": event.moduleVersion } + : {}), + }; + const span = startSpan({ + name: "Cursor Agent", + spanAttributes: { type: SpanTypeAttribute.TASK }, + }); + const startTime = getCurrentUnixTimestamp(); + safeLog(span, { + input: sanitizeUserMessage(message), + metadata, + }); + + const state: CursorRunState = { + activeToolSpans: new Map(), + agent, + conversationText: [], + deltaText: [], + finalized: false, + input: message, + metadata, + metrics: {}, + span, + startTime, + streamMessages: [], + streamText: [], + stepText: [], + taskText: [], + }; + + if (hasCursorCallbacks(sendOptions)) { + event.arguments[1] = wrapSendOptionsCallbacks(sendOptions, state); + } + states.set(event, state); + }, + asyncEnd: (event) => { + const state = states.get(event); + if (!state) { + return; + } + + if (!event.result) { + return; + } + state.run = event.result; + state.metadata = { + ...state.metadata, + ...extractRunMetadata(event.result), + }; + patchCursorRun(event.result, state); + }, + error: (event) => { + const state = states.get(event); + if (!state || !event.error) { + return; + } + safeLog(state.span, { error: event.error.message }); + endOpenToolSpans(state, event.error.message); + state.span.end(); + state.finalized = true; + states.delete(event); + }, + }; + + channel.subscribe(handlers); + this.unsubscribers.push(() => { + channel.unsubscribe(handlers); + }); + } +} + +function patchCursorAgentInPlace(agent: unknown): void { + if (!agent || typeof agent !== "object") { + return; + } + const agentRecord = agent as CursorSDKAgent & Record; + if ( + agentRecord[PATCHED_AGENT] || + agentRecord[WRAPPED_AGENT] || + typeof agentRecord.send !== "function" + ) { + return; + } + + const originalSend = agentRecord.send.bind(agentRecord); + try { + Object.defineProperty(agentRecord, PATCHED_AGENT, { + configurable: false, + enumerable: false, + value: true, + }); + Object.defineProperty(agentRecord, "send", { + configurable: true, + value( + message: string | CursorSDKUserMessage, + options?: CursorSDKSendOptions, + ) { + const args = [message, options] as [ + string | CursorSDKUserMessage, + CursorSDKSendOptions | undefined, + ]; + return cursorSDKChannels.send.tracePromise( + () => originalSend(...args), + { + agent: agentRecord, + arguments: args, + operation: "send", + } as never, + ); + }, + writable: true, + }); + } catch { + // Frozen/sealed agents cannot be patched. Leave user behavior untouched. + } +} + +function wrapSendOptionsCallbacks( + options: CursorSDKSendOptions, + state: CursorRunState, +): CursorSDKSendOptions { + const originalOnDelta = options.onDelta; + const originalOnStep = options.onStep; + + return { + ...options, + async onDelta(args) { + try { + await handleInteractionUpdate(state, args.update); + } catch (error) { + logInstrumentationError("Cursor SDK onDelta", error); + } + if (originalOnDelta) { + return originalOnDelta(args); + } + }, + async onStep(args) { + try { + handleStepUpdate(state, args.step); + } catch (error) { + logInstrumentationError("Cursor SDK onStep", error); + } + if (originalOnStep) { + return originalOnStep(args); + } + }, + }; +} + +function hasCursorCallbacks( + options: CursorSDKSendOptions | undefined, +): options is CursorSDKSendOptions { + return ( + !!options && + (typeof options.onDelta === "function" || + typeof options.onStep === "function") + ); +} + +function patchCursorRun(run: CursorSDKRun, state: CursorRunState): void { + if (!run || typeof run !== "object") { + return; + } + const runRecord = run as CursorSDKRun & Record; + if (runRecord[PATCHED_RUN]) { + return; + } + + try { + Object.defineProperty(runRecord, PATCHED_RUN, { + configurable: false, + enumerable: false, + value: true, + }); + + if (typeof runRecord.stream === "function") { + const originalStream = runRecord.stream.bind(runRecord); + Object.defineProperty(runRecord, "stream", { + configurable: true, + value() { + const stream = originalStream(); + return patchCursorStream(stream, state); + }, + writable: true, + }); + } + + if (typeof runRecord.wait === "function") { + const originalWait = runRecord.wait.bind(runRecord); + Object.defineProperty(runRecord, "wait", { + configurable: true, + async value() { + try { + const result = await originalWait(); + state.lastResult = result; + await finalizeCursorRun(state, { result }); + return result; + } catch (error) { + await finalizeCursorRun(state, { error }); + throw error; + } + }, + writable: true, + }); + } + + if (typeof runRecord.conversation === "function") { + const originalConversation = runRecord.conversation.bind(runRecord); + Object.defineProperty(runRecord, "conversation", { + configurable: true, + async value() { + try { + const conversation = await originalConversation(); + await handleConversation(state, conversation); + await finalizeCursorRun(state); + return conversation; + } catch (error) { + await finalizeCursorRun(state, { error }); + throw error; + } + }, + writable: true, + }); + } + } catch { + // If the Run object is not patchable, finish the span with available data. + void finalizeCursorRun(state, { output: run }); + } +} + +async function* patchCursorStream( + stream: AsyncGenerator, + state: CursorRunState, +): AsyncGenerator { + try { + for await (const message of stream) { + try { + await handleStreamMessage(state, message); + } catch (error) { + logInstrumentationError("Cursor SDK stream", error); + } + yield message; + } + await finalizeCursorRun(state); + } catch (error) { + await finalizeCursorRun(state, { error }); + throw error; + } +} + +async function handleInteractionUpdate( + state: CursorRunState, + update: CursorSDKInteractionUpdate, +): Promise { + switch (update.type) { + case "text-delta": + if (typeof update.text === "string") { + state.deltaText.push(update.text); + } + return; + case "token-delta": + if (typeof update.tokens === "number") { + state.metrics["cursor_sdk.delta_tokens"] = + (state.metrics["cursor_sdk.delta_tokens"] ?? 0) + update.tokens; + } + return; + case "tool-call-started": + case "partial-tool-call": + case "tool-call-completed": + await handleToolUpdate( + state, + update as Extract< + CursorSDKInteractionUpdate, + { + type: + | "tool-call-started" + | "partial-tool-call" + | "tool-call-completed"; + } + >, + ); + return; + case "turn-ended": + addUsageMetrics( + state.metrics, + (update as { usage?: CursorSDKUsage }).usage, + ); + return; + case "summary": + if (typeof update.summary === "string") { + state.taskText.push(update.summary); + } + return; + case "step-completed": + if (typeof update.stepDurationMs === "number") { + state.metrics["cursor_sdk.step_duration_ms"] = + (state.metrics["cursor_sdk.step_duration_ms"] ?? 0) + + update.stepDurationMs; + } + state.metrics["cursor_sdk.steps"] = + (state.metrics["cursor_sdk.steps"] ?? 0) + 1; + return; + default: + return; + } +} + +async function handleToolUpdate( + state: CursorRunState, + update: Extract< + CursorSDKInteractionUpdate, + { type: "tool-call-started" | "partial-tool-call" | "tool-call-completed" } + >, +): Promise { + const callId = update.callId; + if (!callId) { + return; + } + + const toolCall = update.toolCall; + const name = extractToolName(toolCall); + const args = extractToolArgs(toolCall); + const result = extractToolResult(toolCall); + + if ( + update.type === "tool-call-started" || + update.type === "partial-tool-call" + ) { + if (!state.activeToolSpans.has(callId)) { + state.activeToolSpans.set( + callId, + await startToolSpan(state, { + args, + callId, + name, + status: "running", + toolCall, + }), + ); + } + return; + } + + const toolState = + state.activeToolSpans.get(callId) ?? + (await startToolSpan(state, { + args, + callId, + name, + status: "completed", + toolCall, + })); + + finishToolSpan(toolState, { + error: toolCall?.status === "error" ? stringifyUnknown(result) : undefined, + metadata: { + "cursor_sdk.tool.status": toolCall?.status ?? "completed", + }, + output: result, + }); + state.activeToolSpans.delete(callId); +} + +async function handleStreamMessage( + state: CursorRunState, + message: CursorSDKMessage, +): Promise { + state.streamMessages.push(message); + if (message.type === "system") { + const systemMessage = message as Extract< + CursorSDKMessage, + { type: "system" } + >; + state.metadata = { + ...state.metadata, + ...extractModelMetadata(systemMessage.model), + ...(systemMessage.agent_id + ? { "cursor_sdk.agent_id": systemMessage.agent_id } + : {}), + ...(systemMessage.run_id + ? { "cursor_sdk.run_id": systemMessage.run_id } + : {}), + ...(systemMessage.tools + ? { "cursor_sdk.tools": systemMessage.tools } + : {}), + }; + return; + } + + if (message.type === "assistant") { + const assistantMessage = message as Extract< + CursorSDKMessage, + { type: "assistant" } + >; + for (const block of assistantMessage.message?.content ?? []) { + if (block?.type === "text" && typeof block.text === "string") { + state.streamText.push(block.text); + } else if (block?.type === "tool_use" && block.id) { + state.activeToolSpans.set( + block.id, + await startToolSpan(state, { + args: block.input, + callId: block.id, + name: block.name, + status: "running", + }), + ); + } + } + return; + } + + if (message.type === "tool_call") { + await handleToolMessage( + state, + message as Extract, + ); + return; + } + + if (message.type === "task" && typeof message.text === "string") { + state.taskText.push(message.text); + return; + } + + if (message.type === "status" && message.status) { + state.metadata["cursor_sdk.status"] = message.status; + } +} + +async function handleToolMessage( + state: CursorRunState, + message: CursorSDKToolUseMessage, +): Promise { + const callId = message.call_id; + if (!callId) { + return; + } + + if (message.status === "running") { + if (!state.activeToolSpans.has(callId)) { + state.activeToolSpans.set( + callId, + await startToolSpan(state, { + args: message.args, + callId, + name: message.name, + status: message.status, + truncated: message.truncated, + }), + ); + } + return; + } + + const toolState = + state.activeToolSpans.get(callId) ?? + (await startToolSpan(state, { + args: message.args, + callId, + name: message.name, + status: message.status, + truncated: message.truncated, + })); + finishToolSpan(toolState, { + error: + message.status === "error" ? stringifyUnknown(message.result) : undefined, + metadata: { + "cursor_sdk.tool.status": message.status, + }, + output: message.result, + }); + state.activeToolSpans.delete(callId); +} + +async function handleConversation( + state: CursorRunState, + turns: CursorSDKConversationTurn[], +): Promise { + state.conversationOutput = turns; + for (const turn of turns) { + if (turn.type === "agentConversationTurn") { + for (const step of turn.turn?.steps ?? []) { + await handleConversationStep(state, step); + } + } else if (turn.type === "shellConversationTurn") { + const command = turn.turn?.shellCommand?.command; + if (command) { + const callId = `shell:${state.activeToolSpans.size}:${command}`; + const toolState = await startToolSpan(state, { + args: turn.turn?.shellCommand, + callId, + name: "shell", + status: "completed", + }); + finishToolSpan(toolState, { + metadata: { "cursor_sdk.tool.status": "completed" }, + output: turn.turn?.shellOutput, + }); + } + } + } +} + +async function handleConversationStep( + state: CursorRunState, + step: CursorSDKConversationStep, +): Promise { + if ( + step.type === "assistantMessage" && + typeof step.message?.text === "string" + ) { + state.conversationText.push(step.message.text); + return; + } + + if (step.type !== "toolCall") { + return; + } + + const toolCall = step.message; + const callId = + typeof toolCall?.callId === "string" + ? toolCall.callId + : `conversation-tool:${state.activeToolSpans.size}`; + const toolState = await startToolSpan(state, { + args: extractToolArgs(toolCall), + callId, + name: extractToolName(toolCall), + status: toolCall?.status, + toolCall, + }); + finishToolSpan(toolState, { + error: + toolCall?.status === "error" + ? stringifyUnknown(toolCall.result) + : undefined, + metadata: { + "cursor_sdk.tool.status": toolCall?.status ?? "completed", + }, + output: extractToolResult(toolCall), + }); +} + +function handleStepUpdate( + state: CursorRunState, + step: CursorSDKConversationStep, +): void { + state.metrics["cursor_sdk.steps"] = + (state.metrics["cursor_sdk.steps"] ?? 0) + 1; + if (step.type) { + const stepTypes = state.metadata["cursor_sdk.step_types"]; + if (Array.isArray(stepTypes)) { + if (!stepTypes.includes(step.type)) { + stepTypes.push(step.type); + } + } else { + state.metadata["cursor_sdk.step_types"] = [step.type]; + } + } + if ( + step.type === "assistantMessage" && + typeof step.message?.text === "string" + ) { + state.stepText.push(step.message.text); + } +} + +async function startToolSpan( + state: CursorRunState, + args: { + args?: unknown; + callId: string; + name?: string; + status?: string; + toolCall?: CursorSDKToolCall; + truncated?: { args?: boolean; result?: boolean }; + }, +): Promise { + const name = args.name || "unknown"; + const metadata: Record = { + "cursor_sdk.tool.status": args.status, + "gen_ai.tool.call.id": args.callId, + "gen_ai.tool.name": name, + }; + if (args.truncated?.args !== undefined) { + metadata["cursor_sdk.tool.args_truncated"] = args.truncated.args; + } + if (args.truncated?.result !== undefined) { + metadata["cursor_sdk.tool.result_truncated"] = args.truncated.result; + } + + const span = startSpan({ + event: { + input: args.args, + metadata, + }, + name: `tool: ${name}`, + parent: await state.span.export(), + spanAttributes: { type: SpanTypeAttribute.TOOL }, + }); + + let subAgentSpan: Span | undefined; + if (isSubAgentToolName(name)) { + subAgentSpan = startSpan({ + event: { + input: args.args, + metadata: { + "cursor_sdk.subagent.tool_call_id": args.callId, + "gen_ai.tool.name": name, + }, + }, + name: formatSubAgentSpanName(args.toolCall, args.args), + parent: await span.export(), + spanAttributes: { type: SpanTypeAttribute.TASK }, + }); + } + + return { span, subAgentSpan }; +} + +function finishToolSpan( + toolState: ToolState, + result: { + error?: string; + metadata?: Record; + output?: unknown; + }, +): void { + try { + if (result.error) { + safeLog(toolState.span, { + error: result.error, + metadata: result.metadata, + output: result.output, + }); + if (toolState.subAgentSpan) { + safeLog(toolState.subAgentSpan, { + error: result.error, + metadata: result.metadata, + output: result.output, + }); + } + } else { + safeLog(toolState.span, { + metadata: result.metadata, + output: result.output, + }); + if (toolState.subAgentSpan) { + safeLog(toolState.subAgentSpan, { + metadata: result.metadata, + output: result.output, + }); + } + } + } finally { + toolState.subAgentSpan?.end(); + toolState.span.end(); + } +} + +async function finalizeCursorRun( + state: CursorRunState, + params: { + error?: unknown; + output?: unknown; + result?: CursorSDKRunResult; + } = {}, +): Promise { + if (state.finalized) { + return; + } + state.finalized = true; + + const error = params.error; + const result = params.result ?? state.lastResult; + const output = + params.output ?? + result?.result ?? + state.run?.result ?? + (state.conversationText.length > 0 + ? state.conversationText.join("\n") + : undefined) ?? + state.conversationOutput ?? + (state.streamText.length > 0 ? state.streamText.join("") : undefined) ?? + (state.deltaText.length > 0 ? state.deltaText.join("") : undefined) ?? + (state.stepText.length > 0 ? state.stepText.join("\n") : undefined) ?? + (state.taskText.length > 0 ? state.taskText.join("\n") : undefined); + + try { + const metadata = { + ...state.metadata, + ...(state.run ? extractRunMetadata(state.run) : {}), + ...(result ? extractRunResultMetadata(result) : {}), + }; + if (error) { + safeLog(state.span, { + error: error instanceof Error ? error.message : String(error), + metadata, + metrics: { + ...cleanMetrics(state.metrics), + ...buildDurationMetrics(state.startTime), + }, + output, + }); + } else { + safeLog(state.span, { + metadata, + metrics: { + ...cleanMetrics(state.metrics), + ...buildDurationMetrics(state.startTime), + }, + output, + }); + } + } finally { + endOpenToolSpans(state); + state.span.end(); + } +} + +function endOpenToolSpans(state: CursorRunState, error?: string): void { + for (const [, toolState] of state.activeToolSpans) { + finishToolSpan(toolState, { error }); + } + state.activeToolSpans.clear(); +} + +function sanitizeUserMessage( + message: string | CursorSDKUserMessage | undefined, +): unknown { + if (typeof message === "string" || message === undefined) { + return message; + } + return { + ...message, + images: message.images?.map((image) => { + const imageRecord = image as Record; + return { + ...(typeof imageRecord.url === "string" + ? { url: imageRecord.url } + : {}), + ...(typeof imageRecord.mimeType === "string" + ? { mimeType: imageRecord.mimeType } + : {}), + ...(image.dimension ? { dimension: image.dimension } : {}), + hasData: typeof imageRecord.data === "string", + }; + }), + }; +} + +function extractAgentOptionsMetadata( + options: CursorSDKAgentOptions | Partial | undefined, +): Record { + if (!options) { + return {}; + } + + return { + ...extractModelMetadata(options.model), + ...(options.agentId ? { "cursor_sdk.agent_id": options.agentId } : {}), + ...(options.name ? { "cursor_sdk.agent_name": options.name } : {}), + ...(options.local + ? { + "cursor_sdk.runtime": "local", + "cursor_sdk.local.cwd": Array.isArray(options.local.cwd) + ? options.local.cwd.join(",") + : options.local.cwd, + } + : {}), + ...(options.cloud + ? { + "cursor_sdk.runtime": "cloud", + "cursor_sdk.cloud.auto_create_pr": options.cloud.autoCreatePR, + "cursor_sdk.cloud.env_type": options.cloud.env?.type, + "cursor_sdk.cloud.env_name": options.cloud.env?.name, + "cursor_sdk.cloud.repos": options.cloud.repos + ?.map((repo) => repo.url) + .filter((url): url is string => typeof url === "string"), + } + : {}), + }; +} + +function extractSendMetadata( + options: CursorSDKSendOptions | undefined, +): Record { + if (!options) { + return {}; + } + return { + ...extractModelMetadata(options.model), + ...(options.local?.force !== undefined + ? { "cursor_sdk.local.force": options.local.force } + : {}), + }; +} + +function extractAgentMetadata(agent: CursorSDKAgent): Record { + return { + ...(agent.agentId ? { "cursor_sdk.agent_id": agent.agentId } : {}), + ...extractModelMetadata(agent.model), + }; +} + +function extractRunMetadata( + run: CursorSDKRun | undefined, +): Record { + if (!run) { + return {}; + } + return { + ...(run.id ? { "cursor_sdk.run_id": run.id } : {}), + ...(run.agentId ? { "cursor_sdk.agent_id": run.agentId } : {}), + ...(run.status ? { "cursor_sdk.status": run.status } : {}), + ...(run.durationMs !== undefined + ? { "cursor_sdk.duration_ms": run.durationMs } + : {}), + ...extractModelMetadata(run.model), + ...extractGitMetadata(run.git), + }; +} + +function extractRunResultMetadata( + result: CursorSDKRunResult | undefined, +): Record { + if (!result) { + return {}; + } + return { + ...(result.id ? { "cursor_sdk.run_id": result.id } : {}), + ...(result.status ? { "cursor_sdk.status": result.status } : {}), + ...(result.durationMs !== undefined + ? { "cursor_sdk.duration_ms": result.durationMs } + : {}), + ...extractModelMetadata(result.model), + ...extractGitMetadata(result.git), + }; +} + +function extractGitMetadata( + git: CursorSDKRunGitInfo | undefined, +): Record { + const branches = git?.branches; + if (!branches || branches.length === 0) { + return {}; + } + return { + "cursor_sdk.git.branches": branches.map((branch) => ({ + branch: branch.branch, + prUrl: branch.prUrl, + repoUrl: branch.repoUrl, + })), + }; +} + +function extractModelMetadata( + model: CursorSDKModelSelection | undefined, +): Record { + if (!model?.id) { + return {}; + } + return { + model: model.id, + "cursor_sdk.model": model.id, + ...(model.params ? { "cursor_sdk.model.params": model.params } : {}), + }; +} + +function addUsageMetrics( + metrics: Record, + usage: CursorSDKUsage | undefined, +): void { + if (!usage) { + return; + } + if (usage.inputTokens !== undefined) { + metrics.prompt_tokens = (metrics.prompt_tokens ?? 0) + usage.inputTokens; + } + if (usage.outputTokens !== undefined) { + metrics.completion_tokens = + (metrics.completion_tokens ?? 0) + usage.outputTokens; + } + if (usage.cacheReadTokens !== undefined) { + metrics.prompt_cached_tokens = + (metrics.prompt_cached_tokens ?? 0) + usage.cacheReadTokens; + } + if (usage.cacheWriteTokens !== undefined) { + metrics.prompt_cache_creation_tokens = + (metrics.prompt_cache_creation_tokens ?? 0) + usage.cacheWriteTokens; + } + metrics.tokens = + (metrics.prompt_tokens ?? 0) + + (metrics.completion_tokens ?? 0) + + (metrics.prompt_cached_tokens ?? 0) + + (metrics.prompt_cache_creation_tokens ?? 0); +} + +function buildDurationMetrics(startTime: number): Record { + const end = getCurrentUnixTimestamp(); + return { + duration: end - startTime, + end, + start: startTime, + }; +} + +function extractToolName(toolCall: CursorSDKToolCall | undefined): string { + if (!toolCall) { + return "unknown"; + } + if (typeof toolCall.name === "string") { + return toolCall.name; + } + if (typeof toolCall.type === "string") { + return toolCall.type; + } + return "unknown"; +} + +function extractToolArgs(toolCall: CursorSDKToolCall | undefined): unknown { + return toolCall && "args" in toolCall ? toolCall.args : undefined; +} + +function extractToolResult(toolCall: CursorSDKToolCall | undefined): unknown { + return toolCall && "result" in toolCall ? toolCall.result : undefined; +} + +function isSubAgentToolName(name: string): boolean { + return name === "Agent" || name === "Task" || name === "task"; +} + +function formatSubAgentSpanName( + toolCall: CursorSDKToolCall | undefined, + args: unknown, +): string { + const details = (toolCall ?? args) as Record | undefined; + const description = + getString(details, "description") ?? + getString(details, "subagent_type") ?? + getString(details, "type") ?? + getString(details, "name"); + return description ? `Agent: ${description}` : "Agent: sub-agent"; +} + +function getString( + obj: Record | undefined, + key: string, +): string | undefined { + const value = obj?.[key]; + return typeof value === "string" ? value : undefined; +} + +function stringifyUnknown(value: unknown): string { + if (value instanceof Error) { + return value.message; + } + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function safeLog(span: Span, event: Parameters[0]): void { + try { + span.log(event); + } catch (error) { + logInstrumentationError("Cursor SDK span log", error); + } +} + +function logInstrumentationError(context: string, error: unknown): void { + // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. + console.error(`Error processing ${context}:`, error); +} + +function cleanMetrics(metrics: Record): Record { + const cleaned: Record = {}; + for (const [key, value] of Object.entries(metrics)) { + if (value !== undefined && Number.isFinite(value)) { + cleaned[key] = value; + } + } + return cleaned; +} diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 0d3111db4..8e09f214a 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -21,6 +21,8 @@ export interface InstrumentationConfig { google?: boolean; huggingface?: boolean; claudeAgentSDK?: boolean; + cursor?: boolean; + cursorSDK?: boolean; openrouter?: boolean; openrouterAgent?: boolean; mistral?: boolean; @@ -111,6 +113,8 @@ class PluginRegistry { google: true, huggingface: true, claudeAgentSDK: true, + cursor: true, + cursorSDK: true, openrouter: true, openrouterAgent: true, mistral: true, @@ -133,7 +137,11 @@ class PluginRegistry { .filter((s) => s.length > 0); for (const sdk of disabled) { - integrations[sdk] = false; + if (sdk === "cursor-sdk") { + integrations.cursorSDK = false; + } else { + integrations[sdk] = false; + } } } diff --git a/js/src/vendor-sdk-types/cursor-sdk.ts b/js/src/vendor-sdk-types/cursor-sdk.ts new file mode 100644 index 000000000..a8bf1bb70 --- /dev/null +++ b/js/src/vendor-sdk-types/cursor-sdk.ts @@ -0,0 +1,337 @@ +/** + * Vendored types for @cursor/sdk used by Braintrust instrumentation. + * + * Keep this surface intentionally narrow. These types are not exported to SDK + * users and should only cover fields we read, wrap, or log. + */ + +export interface CursorSDKModule { + Agent: CursorSDKAgentClass; + [key: string]: unknown; +} + +export interface CursorSDKAgentClass { + create(options: CursorSDKAgentOptions): Promise; + resume( + agentId: string, + options?: Partial, + ): Promise; + prompt( + message: string | CursorSDKUserMessage, + options?: CursorSDKAgentOptions, + ): Promise; + [key: string]: unknown; +} + +export interface CursorSDKAgent { + readonly agentId?: string; + readonly model?: CursorSDKModelSelection; + send( + message: string | CursorSDKUserMessage, + options?: CursorSDKSendOptions, + ): Promise; + close?: () => void; + reload?: () => Promise; + [Symbol.asyncDispose]?: () => Promise; + [key: string | symbol]: unknown; +} + +export interface CursorSDKAgentOptions { + agentId?: string; + apiKey?: string; + model?: CursorSDKModelSelection; + name?: string; + local?: { + cwd?: string | string[]; + settingSources?: string[]; + sandboxOptions?: { enabled?: boolean }; + }; + cloud?: { + env?: { type?: "cloud" | "pool" | "machine"; name?: string }; + repos?: Array<{ url?: string; startingRef?: string; prUrl?: string }>; + autoCreatePR?: boolean; + workOnCurrentBranch?: boolean; + skipReviewerRequest?: boolean; + }; + mcpServers?: Record; + agents?: Record; + [key: string]: unknown; +} + +export interface CursorSDKAgentDefinition { + description?: string; + prompt?: string; + model?: CursorSDKModelSelection | "inherit"; + mcpServers?: unknown[]; +} + +export interface CursorSDKSendOptions { + model?: CursorSDKModelSelection; + mcpServers?: Record; + onDelta?: (args: { + update: CursorSDKInteractionUpdate; + }) => void | Promise; + onStep?: (args: { step: CursorSDKConversationStep }) => void | Promise; + local?: { + force?: boolean; + }; + [key: string]: unknown; +} + +export interface CursorSDKModelSelection { + id?: string; + params?: Array<{ id?: string; value?: string }>; +} + +export interface CursorSDKUserMessage { + text?: string; + images?: CursorSDKImage[]; + [key: string]: unknown; +} + +export type CursorSDKImage = + | { + url?: string; + dimension?: CursorSDKImageDimension; + [key: string]: unknown; + } + | { + data?: string; + mimeType?: string; + dimension?: CursorSDKImageDimension; + [key: string]: unknown; + }; + +export interface CursorSDKImageDimension { + width?: number; + height?: number; +} + +export type CursorSDKRunStatus = "running" | "finished" | "error" | "cancelled"; + +export type CursorSDKRunOperation = + | "stream" + | "wait" + | "cancel" + | "conversation"; + +export interface CursorSDKRun { + readonly id?: string; + readonly agentId?: string; + readonly status?: CursorSDKRunStatus; + readonly result?: string; + readonly model?: CursorSDKModelSelection; + readonly durationMs?: number; + readonly git?: CursorSDKRunGitInfo; + readonly createdAt?: number; + stream(): AsyncGenerator; + wait(): Promise; + conversation(): Promise; + cancel?: () => Promise; + supports?: (operation: CursorSDKRunOperation) => boolean; + unsupportedReason?: (operation: CursorSDKRunOperation) => string | undefined; + onDidChangeStatus?: ( + listener: (status: CursorSDKRunStatus) => void, + ) => () => void; + [key: string]: unknown; +} + +export interface CursorSDKRunResult { + id?: string; + status?: Exclude; + result?: string; + model?: CursorSDKModelSelection; + durationMs?: number; + git?: CursorSDKRunGitInfo; + [key: string]: unknown; +} + +export interface CursorSDKRunGitInfo { + branches?: Array<{ repoUrl?: string; branch?: string; prUrl?: string }>; +} + +export type CursorSDKMessage = + | CursorSDKSystemMessage + | CursorSDKUserMessageEvent + | CursorSDKAssistantMessage + | CursorSDKThinkingMessage + | CursorSDKToolUseMessage + | CursorSDKStatusMessage + | CursorSDKTaskMessage + | CursorSDKRequestMessage + | { type?: string; [key: string]: unknown }; + +export interface CursorSDKSystemMessage { + type: "system"; + subtype?: "init"; + agent_id?: string; + run_id?: string; + model?: CursorSDKModelSelection; + tools?: string[]; +} + +export interface CursorSDKUserMessageEvent { + type: "user"; + agent_id?: string; + run_id?: string; + message?: { role?: "user"; content?: CursorSDKTextBlock[] }; +} + +export interface CursorSDKAssistantMessage { + type: "assistant"; + agent_id?: string; + run_id?: string; + message?: { + role?: "assistant"; + content?: Array; + }; +} + +export interface CursorSDKThinkingMessage { + type: "thinking"; + agent_id?: string; + run_id?: string; + text?: string; + thinking_duration_ms?: number; +} + +export interface CursorSDKToolUseMessage { + type: "tool_call"; + agent_id?: string; + run_id?: string; + call_id?: string; + name?: string; + status?: "running" | "completed" | "error"; + args?: unknown; + result?: unknown; + truncated?: { args?: boolean; result?: boolean }; +} + +export interface CursorSDKStatusMessage { + type: "status"; + agent_id?: string; + run_id?: string; + status?: string; + message?: string; +} + +export interface CursorSDKTaskMessage { + type: "task"; + agent_id?: string; + run_id?: string; + status?: string; + text?: string; +} + +export interface CursorSDKRequestMessage { + type: "request"; + agent_id?: string; + run_id?: string; + request_id?: string; +} + +export interface CursorSDKTextBlock { + type?: "text"; + text?: string; +} + +export interface CursorSDKToolUseBlock { + type?: "tool_use"; + id?: string; + name?: string; + input?: unknown; +} + +export type CursorSDKInteractionUpdate = + | { + type: "text-delta" | "thinking-delta"; + text?: string; + [key: string]: unknown; + } + | { + type: "thinking-completed"; + thinkingDurationMs?: number; + [key: string]: unknown; + } + | { + type: "tool-call-started" | "partial-tool-call" | "tool-call-completed"; + callId?: string; + toolCall?: CursorSDKToolCall; + modelCallId?: string; + status?: string; + [key: string]: unknown; + } + | { + type: "token-delta"; + tokens?: number; + [key: string]: unknown; + } + | { + type: "turn-ended"; + usage?: CursorSDKUsage; + [key: string]: unknown; + } + | { + type: + | "step-started" + | "step-completed" + | "user-message-appended" + | "summary" + | "summary-started" + | "summary-completed" + | "shell-output-delta"; + [key: string]: unknown; + } + | { type?: string; [key: string]: unknown }; + +export interface CursorSDKUsage { + inputTokens?: number; + outputTokens?: number; + cacheReadTokens?: number; + cacheWriteTokens?: number; +} + +export interface CursorSDKToolCall { + type?: string; + name?: string; + args?: unknown; + result?: unknown; + truncated?: { args?: boolean; result?: boolean }; + status?: "running" | "completed" | "error"; + [key: string]: unknown; +} + +export type CursorSDKConversationTurn = + | { + type?: "agentConversationTurn"; + turn?: { + userMessage?: { text?: string }; + steps?: CursorSDKConversationStep[]; + }; + [key: string]: unknown; + } + | { + type?: "shellConversationTurn"; + turn?: { + shellCommand?: { command?: string; workingDirectory?: string }; + shellOutput?: { stdout?: string; stderr?: string; exitCode?: number }; + }; + [key: string]: unknown; + }; + +export type CursorSDKConversationStep = + | { + type?: "assistantMessage"; + message?: { text?: string }; + [key: string]: unknown; + } + | { + type?: "thinkingMessage"; + message?: { text?: string; thinkingDurationMs?: number }; + [key: string]: unknown; + } + | { + type?: "toolCall"; + message?: CursorSDKToolCall; + [key: string]: unknown; + }; diff --git a/js/src/wrappers/cursor-sdk.test.ts b/js/src/wrappers/cursor-sdk.test.ts new file mode 100644 index 000000000..28ef5a0fe --- /dev/null +++ b/js/src/wrappers/cursor-sdk.test.ts @@ -0,0 +1,145 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { tracePromise } = vi.hoisted(() => ({ + tracePromise: vi.fn((fn: () => Promise) => fn()), +})); + +vi.mock("../isomorph", () => ({ + default: { + newTracingChannel: vi.fn(() => ({ + subscribe: vi.fn(), + tracePromise, + unsubscribe: vi.fn(), + })), + }, +})); + +import { wrapCursorSDK } from "./cursor-sdk"; + +describe("wrapCursorSDK", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns invalid modules unchanged", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const sdk = { Cursor: {} }; + + expect(wrapCursorSDK(sdk)).toBe(sdk); + expect(warnSpy).toHaveBeenCalledWith( + "Unsupported Cursor SDK. Not wrapping.", + ); + + warnSpy.mockRestore(); + }); + + it("wraps Agent.create and returned agent.send", async () => { + const run = makeRun(); + const agent = { + agentId: "agent-1", + send: vi.fn(async () => run), + }; + const sdk = { + Agent: class { + static async create() { + return agent; + } + }, + }; + + const wrapped = wrapCursorSDK(sdk); + const created = await wrapped.Agent.create({ local: { cwd: "/tmp/repo" } }); + const result = await created.send("hello", { + onDelta: vi.fn(), + onStep: vi.fn(), + }); + + expect(result).toBe(run); + expect(agent.send).toHaveBeenCalledWith("hello", expect.any(Object)); + expect(tracePromise).toHaveBeenCalledTimes(2); + }); + + it("wraps Agent.resume and preserves private-field-safe method binding", async () => { + class PrivateAgent { + #run = makeRun(); + agentId = "agent-2"; + + async send() { + return this.#run; + } + + async [Symbol.asyncDispose]() { + return undefined; + } + } + + const sdk = { + Agent: class { + static async resume() { + return new PrivateAgent(); + } + }, + }; + + const wrapped = wrapCursorSDK(sdk); + const agent = await wrapped.Agent.resume("agent-2"); + + await expect(agent.send("hello")).resolves.toMatchObject({ id: "run-1" }); + await expect(agent[Symbol.asyncDispose]()).resolves.toBeUndefined(); + }); + + it("wraps Agent.prompt", async () => { + const sdk = { + Agent: class { + static async prompt(message: string) { + return { id: "run-1", result: message, status: "finished" }; + } + }, + }; + + const wrapped = wrapCursorSDK(sdk); + await expect(wrapped.Agent.prompt("hello")).resolves.toMatchObject({ + result: "hello", + }); + expect(tracePromise).toHaveBeenCalledTimes(1); + }); + + it("handles module namespace-like objects", async () => { + const Agent = class { + static async prompt() { + return { status: "finished" }; + } + }; + const sdk = Object.defineProperty({}, "Agent", { + configurable: false, + enumerable: true, + value: Agent, + writable: false, + }); + + const wrapped = wrapCursorSDK(sdk as { Agent: typeof Agent }); + + await expect(wrapped.Agent.prompt("hello")).resolves.toMatchObject({ + status: "finished", + }); + }); +}); + +function makeRun() { + return { + agentId: "agent-1", + async conversation() { + return []; + }, + id: "run-1", + stream: async function* () { + yield { + type: "assistant", + message: { content: [{ text: "hello", type: "text" }] }, + }; + }, + async wait() { + return { id: "run-1", result: "hello", status: "finished" }; + }, + }; +} diff --git a/js/src/wrappers/cursor-sdk.ts b/js/src/wrappers/cursor-sdk.ts new file mode 100644 index 000000000..0a763e906 --- /dev/null +++ b/js/src/wrappers/cursor-sdk.ts @@ -0,0 +1,182 @@ +import { cursorSDKChannels } from "../instrumentation/plugins/cursor-sdk-channels"; +import type { + CursorSDKAgent, + CursorSDKAgentClass, + CursorSDKAgentOptions, + CursorSDKModule, + CursorSDKRunResult, + CursorSDKSendOptions, + CursorSDKUserMessage, +} from "../vendor-sdk-types/cursor-sdk"; + +const WRAPPED_AGENT = Symbol.for("braintrust.cursor-sdk.wrapped-agent"); + +/** + * Wraps the Cursor TypeScript SDK with Braintrust tracing. The wrapper emits + * diagnostics-channel events; the Cursor SDK plugin owns span lifecycle. + */ +export function wrapCursorSDK(sdk: T): T { + if (!sdk || typeof sdk !== "object") { + return sdk; + } + + const maybeSDK = sdk as Record; + if (!maybeSDK.Agent || typeof maybeSDK.Agent !== "function") { + // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. + console.warn("Unsupported Cursor SDK. Not wrapping."); + return sdk; + } + + const target = isModuleNamespace(sdk) + ? Object.setPrototypeOf({}, sdk) + : (sdk as Record); + + return new Proxy(target, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop === "Agent" && typeof value === "function") { + return wrapCursorAgentClass(value as unknown as CursorSDKAgentClass); + } + if (typeof value === "function") { + return value.bind(target); + } + return value; + }, + }) as T; +} + +function isModuleNamespace(obj: unknown): boolean { + if (!obj || typeof obj !== "object") { + return false; + } + if (obj.constructor?.name === "Module") { + return true; + } + const keys = Object.keys(obj); + if (keys.length === 0) { + return false; + } + const descriptor = Object.getOwnPropertyDescriptor(obj, keys[0]); + return descriptor ? !descriptor.configurable && !descriptor.writable : false; +} + +function wrapCursorAgentClass(Agent: CursorSDKAgentClass): CursorSDKAgentClass { + const cache = new Map(); + + return new Proxy(Agent, { + get(target, prop, receiver) { + if (cache.has(prop)) { + return cache.get(prop); + } + + const value = Reflect.get(target, prop, receiver); + if (prop === "create" && typeof value === "function") { + const wrapped = async function ( + options: CursorSDKAgentOptions, + ): Promise { + const args = [options] as [CursorSDKAgentOptions]; + return cursorSDKChannels.create.tracePromise( + async () => + wrapCursorAgent(await Reflect.apply(value, target, args)), + { arguments: args } as never, + ); + }; + cache.set(prop, wrapped); + return wrapped; + } + + if (prop === "resume" && typeof value === "function") { + const wrapped = async function ( + agentId: string, + options?: Partial, + ): Promise { + const args = [agentId, options] as [ + string, + Partial | undefined, + ]; + return cursorSDKChannels.resume.tracePromise( + async () => + wrapCursorAgent(await Reflect.apply(value, target, args)), + { arguments: args } as never, + ); + }; + cache.set(prop, wrapped); + return wrapped; + } + + if (prop === "prompt" && typeof value === "function") { + const wrapped = async function ( + message: string | CursorSDKUserMessage, + options?: CursorSDKAgentOptions, + ): Promise { + const args = [message, options] as [ + string | CursorSDKUserMessage, + CursorSDKAgentOptions | undefined, + ]; + return cursorSDKChannels.prompt.tracePromise( + () => Reflect.apply(value, target, args), + { arguments: args } as never, + ); + }; + cache.set(prop, wrapped); + return wrapped; + } + + if (typeof value === "function") { + const bound = value.bind(target); + cache.set(prop, bound); + return bound; + } + + return value; + }, + }) as CursorSDKAgentClass; +} + +export function wrapCursorAgent(agent: CursorSDKAgent): CursorSDKAgent { + if (!agent || typeof agent !== "object") { + return agent; + } + if ((agent as Record)[WRAPPED_AGENT]) { + return agent; + } + + const proxy = new Proxy(agent, { + get(target, prop, receiver) { + if (prop === WRAPPED_AGENT) { + return true; + } + + const value = Reflect.get(target, prop, receiver); + if (prop === "send" && typeof value === "function") { + return function ( + message: string | CursorSDKUserMessage, + options?: CursorSDKSendOptions, + ) { + const args = [message, options] as [ + string | CursorSDKUserMessage, + CursorSDKSendOptions | undefined, + ]; + return cursorSDKChannels.send.tracePromise( + () => Reflect.apply(value, target, args), + { + agent: target, + arguments: args, + operation: "send", + } as never, + ); + }; + } + + if (typeof value === "function") { + return value.bind(target); + } + + return value; + }, + }); + + return proxy as CursorSDKAgent; +} + +export type { CursorSDKModule }; diff --git a/turbo.json b/turbo.json index bad5b66ae..809a67834 100644 --- a/turbo.json +++ b/turbo.json @@ -6,6 +6,7 @@ "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENROUTER_API_KEY", "MISTRAL_API_KEY", @@ -27,6 +28,7 @@ "BRAINTRUST_API_KEY", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", @@ -46,6 +48,7 @@ "BRAINTRUST_E2E_RUN_CONTEXT_DIR", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", @@ -70,6 +73,7 @@ "BRAINTRUST_E2E_RUN_CONTEXT_DIR", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", @@ -89,6 +93,7 @@ "BRAINTRUST_E2E_RUN_CONTEXT_DIR", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", @@ -122,6 +127,7 @@ "BRAINTRUST_API_KEY", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", @@ -137,6 +143,7 @@ "BRAINTRUST_API_KEY", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", @@ -152,6 +159,7 @@ "BRAINTRUST_API_KEY", "GEMINI_API_KEY", "COHERE_API_KEY", + "CURSOR_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", From 3c9f5fad4d7316cc1e15496726dca626b8c9a630 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 30 Apr 2026 12:30:19 +0200 Subject: [PATCH 2/6] cs --- .changeset/short-moles-punch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/short-moles-punch.md diff --git a/.changeset/short-moles-punch.md b/.changeset/short-moles-punch.md new file mode 100644 index 000000000..a7a2b2261 --- /dev/null +++ b/.changeset/short-moles-punch.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat: Add @cursor/sdk instrumentation From 0137f481b493f728d5ee85483c15d393803bcdd1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 30 Apr 2026 13:01:08 +0200 Subject: [PATCH 3/6] fix knip --- js/src/wrappers/cursor-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/wrappers/cursor-sdk.ts b/js/src/wrappers/cursor-sdk.ts index 0a763e906..a35d0f090 100644 --- a/js/src/wrappers/cursor-sdk.ts +++ b/js/src/wrappers/cursor-sdk.ts @@ -133,7 +133,7 @@ function wrapCursorAgentClass(Agent: CursorSDKAgentClass): CursorSDKAgentClass { }) as CursorSDKAgentClass; } -export function wrapCursorAgent(agent: CursorSDKAgent): CursorSDKAgent { +function wrapCursorAgent(agent: CursorSDKAgent): CursorSDKAgent { if (!agent || typeof agent !== "object") { return agent; } From 6ac297860284026ffde65a25d16f22144d35a621 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 30 Apr 2026 16:46:21 +0200 Subject: [PATCH 4/6] no dotenv --- e2e/scenarios/cursor-sdk-instrumentation/package.json | 3 +-- e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml | 9 --------- .../cursor-sdk-instrumentation/scenario.impl.mjs | 7 ------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/e2e/scenarios/cursor-sdk-instrumentation/package.json b/e2e/scenarios/cursor-sdk-instrumentation/package.json index 4cf9f2128..44193c30f 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/package.json +++ b/e2e/scenarios/cursor-sdk-instrumentation/package.json @@ -9,8 +9,7 @@ } }, "dependencies": { - "cursor-sdk-v1": "npm:@cursor/sdk@1.0.10", - "dotenv": "^17.2.3" + "cursor-sdk-v1": "npm:@cursor/sdk@1.0.10" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml b/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml index ce4bdb412..2917de861 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml +++ b/e2e/scenarios/cursor-sdk-instrumentation/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: cursor-sdk-v1: specifier: npm:@cursor/sdk@1.0.10 version: '@cursor/sdk@1.0.10' - dotenv: - specifier: ^17.2.3 - version: 17.4.2 packages: @@ -180,10 +177,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -720,8 +713,6 @@ snapshots: detect-libc@2.1.2: {} - dotenv@17.4.2: {} - emoji-regex@8.0.0: optional: true diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs index 1270184e4..e1f3e2320 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs @@ -1,5 +1,3 @@ -import { config as loadDotEnv } from "dotenv"; -import { resolve } from "node:path"; import { traced, wrapCursorSDK } from "braintrust"; import { collectAsync, @@ -7,11 +5,6 @@ import { runTracedScenario, } from "../../helpers/provider-runtime.mjs"; -loadDotEnv({ - path: resolve(process.env.BRAINTRUST_E2E_REPO_ROOT ?? process.cwd(), ".env"), - quiet: true, -}); - const CURSOR_MODEL = "composer-2"; export const ROOT_NAME = "cursor-sdk-root"; From a9f40a23cbb31debbfdc615bee2bd625f1814706 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 4 May 2026 10:26:31 -0700 Subject: [PATCH 5/6] Add scenario for subagents --- .../cursor-sdk-v1-auto-hook.span-events.json | 45 ++++++++++--- .../cursor-sdk-v1-wrapped.span-events.json | 45 ++++++++++--- .../cursor-sdk-instrumentation/assertions.ts | 63 +++++++++++++++++-- .../scenario.impl.mjs | 2 +- 4 files changed, 136 insertions(+), 19 deletions(-) diff --git a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json index 1d38061df..1308581f3 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json +++ b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-auto-hook.span-events.json @@ -108,7 +108,38 @@ ], "type": null }, - "subagent_task": null, + "subagent_task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "task" + }, + "metric_keys": [], + "name": "Agent: ", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + "subagent_tool": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "task" + }, + "metric_keys": [], + "name": "tool: task", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "tool" + }, "task": { "has_input": true, "has_output": true, @@ -126,7 +157,7 @@ ], "name": "Cursor Agent", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ "" ], @@ -142,9 +173,9 @@ "metric_keys": [], "name": "tool: shell", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ - "" + "" ], "type": "tool" } @@ -159,7 +190,7 @@ "metric_keys": [], "name": "cursor-sdk-wait-operation", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ "" ], @@ -193,9 +224,9 @@ ], "name": "Cursor Agent", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ - "" + "" ], "type": "task" } diff --git a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json index 1d38061df..1308581f3 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json +++ b/e2e/scenarios/cursor-sdk-instrumentation/__snapshots__/cursor-sdk-v1-wrapped.span-events.json @@ -108,7 +108,38 @@ ], "type": null }, - "subagent_task": null, + "subagent_task": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "task" + }, + "metric_keys": [], + "name": "Agent: ", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "task" + }, + "subagent_tool": { + "has_input": true, + "has_output": true, + "metadata": { + "cursor_sdk.tool.status": "completed", + "gen_ai.tool.name": "task" + }, + "metric_keys": [], + "name": "tool: task", + "root_span_id": "", + "span_id": "", + "span_parents": [ + "" + ], + "type": "tool" + }, "task": { "has_input": true, "has_output": true, @@ -126,7 +157,7 @@ ], "name": "Cursor Agent", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ "" ], @@ -142,9 +173,9 @@ "metric_keys": [], "name": "tool: shell", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ - "" + "" ], "type": "tool" } @@ -159,7 +190,7 @@ "metric_keys": [], "name": "cursor-sdk-wait-operation", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ "" ], @@ -193,9 +224,9 @@ ], "name": "Cursor Agent", "root_span_id": "", - "span_id": "", + "span_id": "", "span_parents": [ - "" + "" ], "type": "task" } diff --git a/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts index 16363d8b8..85016fb83 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts +++ b/e2e/scenarios/cursor-sdk-instrumentation/assertions.ts @@ -70,6 +70,9 @@ function summarizeSpan(event: CapturedLogEvent | undefined): Json { if (typeof event.row.error === "string") { summary.error = event.row.error; } + if (typeof summary.name === "string" && summary.name.startsWith("Agent:")) { + summary.name = "Agent: "; + } return summary; } @@ -82,6 +85,42 @@ function findCursorTask(events: CapturedLogEvent[], operationName: string) { return findChildSpans(events, "Cursor Agent", operation?.span.id).at(-1); } +function findSubagentTool( + events: CapturedLogEvent[], + parentId: string | undefined, +) { + if (!parentId) { + return undefined; + } + return [...events] + .reverse() + .find( + (event) => + event.span.type === "tool" && + event.span.parentIds.includes(parentId) && + ["tool: Agent", "tool: Task", "tool: task"].includes( + event.span.name ?? "", + ), + ); +} + +function findSubagentTask( + events: CapturedLogEvent[], + parentId: string | undefined, +) { + if (!parentId) { + return undefined; + } + return [...events] + .reverse() + .find( + (event) => + event.span.type === "task" && + event.span.parentIds.includes(parentId) && + event.span.name?.startsWith("Agent:"), + ); +} + function outputText(event: CapturedLogEvent | undefined): string { return typeof event?.output === "string" ? event.output : ""; } @@ -95,10 +134,8 @@ function summarize(events: CapturedLogEvent[]): Json { "cursor-sdk-resume-conversation-operation", ); const tool = findAllSpans(events, "tool: shell").at(-1); - const subagentTask = events.find( - (event) => - event.span.type === "task" && event.span.name?.startsWith("Agent:"), - ); + const subagentTool = findSubagentTool(events, streamTask?.span.id); + const subagentTask = findSubagentTask(events, subagentTool?.span.id); return normalizeForSnapshot({ conversation: { @@ -119,6 +156,7 @@ function summarize(events: CapturedLogEvent[]): Json { findOperation(events, "cursor-sdk-stream-operation"), ), subagent_task: summarizeSpan(subagentTask), + subagent_tool: summarizeSpan(subagentTool), task: summarizeSpan(streamTask), tool: summarizeSpan(tool), }, @@ -213,6 +251,23 @@ export function defineCursorSDKInstrumentationAssertions(options: { }, ); + test("captures subagent spans when Cursor uses agents", testConfig, () => { + const streamTask = findCursorTask(events, "cursor-sdk-stream-operation"); + const subagentTool = findSubagentTool(events, streamTask?.span.id); + const subagentTask = findSubagentTask(events, subagentTool?.span.id); + + expect(subagentTool).toBeDefined(); + expect(subagentTool?.metadata).toMatchObject({ + "cursor_sdk.tool.status": "completed", + }); + expect(subagentTask).toBeDefined(); + expect(subagentTask?.span.rootId).toBe(streamTask?.span.rootId); + expect(subagentTask?.metadata).toMatchObject({ + "cursor_sdk.tool.status": "completed", + }); + expect(subagentTask?.output).toBeDefined(); + }); + test("preserves user onDelta/onStep callbacks", testConfig, () => { expect(findLatestSpan(events, "cursor-sdk-user-on-delta")).toBeDefined(); expect(findLatestSpan(events, "cursor-sdk-user-on-step")).toBeDefined(); diff --git a/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs index e1f3e2320..4dc909eb9 100644 --- a/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/cursor-sdk-instrumentation/scenario.impl.mjs @@ -61,7 +61,7 @@ async function runCursorSDKScenario({ decorateSDK, sdk }) { }, }); const run = await reusableAgent.send( - "Run the shell command `printf cursor_tool_ok` and report the output. Do not edit files.", + "First use the reviewer subagent to confirm exactly CURSOR_SUBAGENT_OK. Then run the shell command `printf cursor_tool_ok` and report the output. Do not edit files.", ); await collectAsync(run.stream()); }); From c9c7308bc666e51a7d13f5e772f096a9cb4c5bde Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 4 May 2026 11:12:33 -0700 Subject: [PATCH 6/6] logger --- js/src/instrumentation/plugins/cursor-sdk-plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts index 1788e4f6e..5f45bf440 100644 --- a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts @@ -1,6 +1,7 @@ import { BasePlugin } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; +import { debugLogger } from "../../debug-logger"; import { startSpan } from "../../logger"; import type { Span } from "../../logger"; import { getCurrentUnixTimestamp } from "../../util"; @@ -1164,8 +1165,7 @@ function safeLog(span: Span, event: Parameters[0]): void { } function logInstrumentationError(context: string, error: unknown): void { - // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. - console.error(`Error processing ${context}:`, error); + debugLogger.error(`Error processing ${context}:`, error); } function cleanMetrics(metrics: Record): Record {