Summary
Cursor's Python SDK (cursor-sdk, latest version 1.0.26) exposes Cursor's coding-agent runtime programmatically with synchronous and asynchronous clients. This repository currently has no dedicated Cursor SDK support: no integration package, setup_cursor_sdk() entry point, auto-instrumentation wiring, provider version matrix, nox session, cassette/transport recordings, examples, or focused tests.
The SDK is a close parallel to our existing Claude Agent SDK integration. Users can create or resume local and cloud agents, submit prompts, stream normalized messages or raw events, observe tool-call lifecycles, execute custom Python tools, inspect token usage, and wait for terminal run results. Production workflows built on cursor-sdk should get Braintrust traces for agent runs, model calls, tool executions, token usage, errors, cancellation, and final results.
Specification requirement
The implementation must follow the Braintrust instrumentation-spec skill and its canonical instrumentation guide, including the linked specifications for token and cost metrics, attachments, and tool approval metadata.
This issue does not authorize new metadata, metric keys, span types, or captured fields. If Cursor-specific fields are needed but not allowed by the instrumentation specification, update braintrustdata/braintrust-spec first rather than inventing cursor_sdk.* telemetry fields here.
Relevant SDK surfaces
The Cursor Python SDK exposes these important sync and async surfaces:
| SDK surface |
Description |
Agent.create() / AsyncAgent.create() |
Create a local or Cursor-hosted cloud agent |
Agent.prompt() / AsyncAgent.prompt() |
One-shot create/send/wait/dispose flow |
Agent.resume() / AsyncAgent.resume() |
Reattach to an existing local or cloud agent |
agent.send() |
Start a run with per-run model, mode, MCP servers, environment variables, on_delta, and on_step options |
run.messages() / run.stream() |
Sync or async stream of normalized SDKMessage events (assistant, thinking, tool_call, status, task, request, usage, etc.) |
run.events() / direct run iteration / run.observe() |
Lower-level RunStreamEvent envelopes and resumable observation |
run.iter_text() / run.text() / run.wait() |
Consume text or wait for the terminal RunResult |
run.cancel() / run.conversation() |
Cancel a run or inspect its structured conversation |
CursorClient / AsyncClient resource namespaces |
Explicit lifecycle and client.agents.create() / resume() flows |
CustomTool.execute |
User-defined Python tool execution for local agents |
These are candidate patch/lifecycle surfaces, not a requirement to create a span for every method. CRUD/catalog/history/artifact calls such as list(), get(), list_runs(), get_run(), and download_artifact() should not become llm or tool spans merely because they are available. Instrument the AI-generating agent run and its model/tool children.
No coverage in any Braintrust instrumentation layer:
- No
py/src/braintrust/integrations/cursor_sdk/ integration
- No
setup_cursor_sdk() or manual wrapping helper
- No
CursorSDKIntegration export or auto_instrument() wiring
- No
cursor-sdk entry in py/pyproject.toml's provider matrix or cassette-directory map
- No dedicated nox session
- No examples, focused tests, or versioned transport/VCR coverage
A case-insensitive grep for cursor-sdk, cursor_sdk, and Cursor SDK concepts under py/ returns no SDK-specific matches.
Desired experience
Suggested API, modeled after setup_claude_agent_sdk():
import os
from cursor_sdk import Agent, LocalAgentOptions
from braintrust.integrations.cursor_sdk import setup_cursor_sdk
setup_cursor_sdk(project="my-project")
with Agent.create(
model="composer-2.5",
local=LocalAgentOptions(cwd=os.getcwd()),
) as agent:
result = agent.send("Summarize what this repository does").wait()
print(result.result)
The async client should produce an equivalent trace:
from cursor_sdk import AsyncClient, LocalAgentOptions
async with await AsyncClient.launch_bridge(workspace=".") as client:
async with await client.agents.create(
model="composer-2.5",
local=LocalAgentOptions(cwd="."),
) as agent:
run = await agent.send("Summarize what this repository does")
print(await run.text())
braintrust.auto_instrument() should also patch cursor_sdk whether it is imported before or after instrumentation setup.
Required span shape
Cursor is an agentic API, so it must follow the specification's agentic span tree without creating duplicate nested llm spans.
First, add a real VCR-backed characterization test that runs the Cursor SDK with relevant downstream provider instrumentation enabled and asserts the emitted span tree. Use that test to determine whether a Cursor model turn already contains a lower-level provider llm leaf (for example, an OpenAI llm span).
If no lower-level provider span is emitted:
task "Cursor Agent" # one parent for one prompt/run
├── llm <Cursor model turn 1> # Cursor owns the observable model call
├── tool <tool name> # one per actual tool execution
├── llm <Cursor model turn 2>
└── ...
If a lower-level provider llm span is emitted, the Cursor span around that call must be a task, not another llm:
task "Cursor Agent"
├── task <Cursor model turn 1> # orchestration/framework span
│ └── llm <provider call 1> # OpenAI/Anthropic/etc. leaf owns LLM telemetry
├── tool <tool name>
├── task <Cursor model turn 2>
│ └── llm <provider call 2>
└── ...
Do not emit two nested llm spans for one provider request.
Parent run span
span_attributes.type = "task"
- A stable descriptive name such as
Cursor Agent
input: the initial user prompt/message that started this run
output: the final response after the agent/tool loop completes
metrics: aggregate canonical token metrics across child LLM calls when available
- Lifetime: from prompt submission until terminal completion, error, cancellation, or expiration
- Provider errors must propagate unchanged and be logged in the top-level
error field
Do not add spans for individual assistant/status/request/usage stream events unless an event corresponds to a model call or actual tool execution required by the spec. Stream events should be used to reconstruct the specified parent/child spans, not produce an event-shaped trace tree.
Child model-call spans and leaf ownership
For each model call/turn, choose the span type from the VCR-observed hierarchy:
- No downstream provider
llm leaf: emit a Cursor llm span with the messages sent for that call, the complete response, resolved metadata.model, metadata.provider = "cursor", allowed metadata.tools, and canonical per-call token metrics.
- Downstream provider
llm leaf exists: emit the Cursor model-turn wrapper as task, keep model/provider attribution when available, and let the lower-level provider llm span own canonical LLM input/output, token metrics, streaming metrics, and cost attribution. Do not copy those metrics onto the intermediate Cursor task.
The lower-level leaf must retain its own provider integration origin (for example, openai-auto); Cursor instrumentation must not overwrite it with cursor-sdk-auto.
Cursor does not have a dedicated provider-native payload exception in the specification, so any Cursor-owned llm input/output should use the canonical OpenAI Chat Completions shape. If the SDK/bridge does not expose enough information to identify model-call boundaries or reconstruct a compliant Cursor-owned leaf, investigate lower-level bridge events. Do not create a token-less Cursor llm span above another provider llm span.
Child tool spans
Each actual model-initiated tool execution must produce exactly one tool child span:
span_attributes.name: tool/function name
input: model-provided tool arguments
output: tool return value
- failed execution: non-null top-level
error
metadata.tool_approval: only approved or denied, and only when Cursor exposes a reliable approval/denial signal for that exact interaction
Correlate Cursor's repeated SDKToolUseMessage start/completion/error events internally so they create one span rather than one span per event. This applies to built-in, MCP, subagent/delegation, shell, and Python CustomTool.execute calls when the SDK exposes actual execution boundaries. Preserve custom-tool return values, exceptions, and Braintrust context.
Do not infer approval from a missing completion event, and do not use approval metadata to represent execution success/failure.
Payload, metrics, and streaming requirements
Token metrics
Use only canonical Braintrust metric names and omit values Cursor did not report; do not fabricate zeroes. Based on Cursor's documented TokenUsage semantics, verify and map per-turn usage as follows:
prompt_tokens = input_tokens + cache_read_tokens + cache_write_tokens
completion_tokens = output_tokens
tokens = prompt_tokens + completion_tokens
prompt_cached_tokens = cache_read_tokens
prompt_cache_creation_tokens = cache_write_tokens
completion_reasoning_tokens = reasoning_tokens when reported
All token counts must be non-negative integers. Cache-read and cache-write counts are subsets of prompt_tokens, not additional tokens beyond it. Apply this per-turn mapping only when Cursor owns the llm leaf. If a lower-level provider llm span exists, that leaf owns per-call token and cost telemetry; the intermediate Cursor model-turn task must not duplicate it. The top-level Cursor run task may still aggregate run usage for display. Do not call agent.get_usage() solely to enrich traces or emit cost data that was not returned by the observed run.
For streaming spans, measure time_to_first_token in seconds from request start to the first generated chunk. Use start/end for timing; do not invent duration_ms or other metric keys.
Streaming behavior
Streaming must accumulate into the same complete span shape as non-streaming execution while preserving Cursor's one-shot stream semantics. Instrumentation must not eagerly consume, double-drain, buffer in a way that changes backpressure, or change sync/async iterator and return types.
A run exposes several mutually consuming interfaces (messages, events, direct iteration, iter_text, text, and wait). Whichever path the user chooses must finalize each run/model/tool span exactly once on normal completion, error, cancellation, or expiration.
User-provided on_delta and on_step callbacks must be preserved. Sync callbacks run inline; async callbacks may be sync or async, and awaitable returns must still be awaited before the next event is processed.
Multimodal input
For UserMessage images:
- replace inline bytes/base64/data URLs in
input with Braintrust attachment references at the original media leaf
- use the canonical
image_url.url placement for normalized image content
- preserve remote URLs without fetching them solely to create attachments
- if conversion fails, preserve the original payload and never fail the user's Cursor call
- never add a separate top-level or metadata attachment list
API keys, bridge bearer tokens, environment-variable values, MCP authorization headers, and other credentials must never be captured.
Reasoning
Capture provider-surfaced reasoning summaries using the specification's reasoning output structure and map reported reasoning usage to completion_reasoning_tokens. Include prior reasoning context in subsequent model-call inputs when Cursor exposes it. Do not invent a Cursor-specific reasoning metric or capture additional hidden chain-of-thought fields outside the specification.
Metadata and provenance
Only emit metadata allowed by the instrumentation specification. In particular, do not emit ad hoc cursor_sdk.agent_id, cursor_sdk.run_id, runtime, status, duration, repository, branch, PR, or mode metadata without first adding it to braintrust-spec.
Every integration-created span must include stable origin provenance with:
context.span_origin.instrumentation.name = "cursor-sdk-auto"
Implementation parallels with Claude Agent SDK
Cursor SDK support can likely follow the existing integrations architecture:
CursorSDKIntegration built on BaseIntegration
- patch stable public classes/functions while handling imports that occur before setup
- a
setup_cursor_sdk() entry point plus auto_instrument() support
- tracing helpers that own run state across send/stream/wait operations
- explicit version pins in
[tool.braintrust.matrix], a dedicated nox session, and versioned recordings
- subprocess auto-instrumentation coverage
Differences to account for:
- Cursor has both sync and async mirrors (
Agent/AsyncAgent, Run/AsyncRun, CursorClient/AsyncClient)
- static helpers, explicit client resource namespaces, and aliases all reach the same underlying surfaces
- the Python package talks to a bundled bridge over loopback Connect/protobuf and may spawn a subprocess; start with a real VCR-backed test and use a transport-level recorder only if standard VCR cannot observe the relevant bridge traffic
- custom Python tools use callback transport from the bridge and should retain Braintrust context
Testing / acceptance criteria
- Before implementation, add a real-package VCR-backed characterization test that enables relevant lower-level provider instrumentation and records/asserts the actual span hierarchy
- The characterization test determines whether each Cursor model-turn span is a Cursor-owned
llm leaf or a task above an existing provider llm leaf; never allow nested llm spans for one request
- Versioned integration coverage for both sync and async APIs, retaining VCR playback coverage in CI
Agent.create().send().messages() and AsyncAgent.create().send().messages()
Agent.prompt() / AsyncAgent.prompt()
Agent.resume().send().wait() and explicit client.agents.* flows
- every run consumption path (
messages, events, direct iteration, iter_text, text, wait, observe) finalizes spans once and preserves return/iterator types
- exact ordered agentic hierarchy, including parallel tools where applicable
- when Cursor owns the leaf: canonical child
llm input/output, metadata.model, metadata.provider = "cursor", and canonical per-turn token metrics
- when a downstream leaf exists: Cursor model-turn
task → provider llm, with leaf-owned metrics/provider provenance and no duplicated intermediate LLM telemetry
- parent input/output and correctly aggregated token metrics
- callback preservation for
on_delta and on_step, including awaited async callbacks
- tool start/completion/error correlation, top-level tool errors, optional normalized approval metadata, and
CustomTool.execute tracing
- attachment conversion and secret-redaction assertions
time_to_first_token for streaming and reasoning/cache metric mapping
- cancellation, expiration, and provider error propagation
- sync/async context-manager cleanup and no leaked bridge process
- instrumentation failures never alter the user's Cursor call
context.span_origin.instrumentation.name == "cursor-sdk-auto" on every integration-created span without leaking that origin onto user-created nested spans
- assertions that no unapproved metadata or metric keys are emitted
- setup idempotence, import-before-setup behavior, and subprocess
auto_instrument() coverage
Upstream references
Local files inspected
py/src/braintrust/integrations/ — no Cursor SDK integration
py/src/braintrust/wrappers/ — no Cursor SDK compatibility wrapper
py/src/braintrust/auto.py — no Cursor SDK auto-instrumentation
py/pyproject.toml — no cursor-sdk matrix or cassette mapping
py/noxfile.py — no Cursor SDK test session
- Repo-wide case-insensitive grep under
py/ — no Cursor SDK-specific matches
Summary
Cursor's Python SDK (
cursor-sdk, latest version1.0.26) exposes Cursor's coding-agent runtime programmatically with synchronous and asynchronous clients. This repository currently has no dedicated Cursor SDK support: no integration package,setup_cursor_sdk()entry point, auto-instrumentation wiring, provider version matrix, nox session, cassette/transport recordings, examples, or focused tests.The SDK is a close parallel to our existing Claude Agent SDK integration. Users can create or resume local and cloud agents, submit prompts, stream normalized messages or raw events, observe tool-call lifecycles, execute custom Python tools, inspect token usage, and wait for terminal run results. Production workflows built on
cursor-sdkshould get Braintrust traces for agent runs, model calls, tool executions, token usage, errors, cancellation, and final results.Specification requirement
The implementation must follow the Braintrust instrumentation-spec skill and its canonical instrumentation guide, including the linked specifications for token and cost metrics, attachments, and tool approval metadata.
This issue does not authorize new metadata, metric keys, span types, or captured fields. If Cursor-specific fields are needed but not allowed by the instrumentation specification, update
braintrustdata/braintrust-specfirst rather than inventingcursor_sdk.*telemetry fields here.Relevant SDK surfaces
The Cursor Python SDK exposes these important sync and async surfaces:
Agent.create()/AsyncAgent.create()Agent.prompt()/AsyncAgent.prompt()Agent.resume()/AsyncAgent.resume()agent.send()on_delta, andon_stepoptionsrun.messages()/run.stream()SDKMessageevents (assistant,thinking,tool_call,status,task,request,usage, etc.)run.events()/ direct run iteration /run.observe()RunStreamEventenvelopes and resumable observationrun.iter_text()/run.text()/run.wait()RunResultrun.cancel()/run.conversation()CursorClient/AsyncClientresource namespacesclient.agents.create()/resume()flowsCustomTool.executeThese are candidate patch/lifecycle surfaces, not a requirement to create a span for every method. CRUD/catalog/history/artifact calls such as
list(),get(),list_runs(),get_run(), anddownload_artifact()should not becomellmortoolspans merely because they are available. Instrument the AI-generating agent run and its model/tool children.No coverage in any Braintrust instrumentation layer:
py/src/braintrust/integrations/cursor_sdk/integrationsetup_cursor_sdk()or manual wrapping helperCursorSDKIntegrationexport orauto_instrument()wiringcursor-sdkentry inpy/pyproject.toml's provider matrix or cassette-directory mapA case-insensitive grep for
cursor-sdk,cursor_sdk, and Cursor SDK concepts underpy/returns no SDK-specific matches.Desired experience
Suggested API, modeled after
setup_claude_agent_sdk():The async client should produce an equivalent trace:
braintrust.auto_instrument()should also patchcursor_sdkwhether it is imported before or after instrumentation setup.Required span shape
Cursor is an agentic API, so it must follow the specification's agentic span tree without creating duplicate nested
llmspans.First, add a real VCR-backed characterization test that runs the Cursor SDK with relevant downstream provider instrumentation enabled and asserts the emitted span tree. Use that test to determine whether a Cursor model turn already contains a lower-level provider
llmleaf (for example, an OpenAIllmspan).If no lower-level provider span is emitted:
If a lower-level provider
llmspan is emitted, the Cursor span around that call must be atask, not anotherllm:Do not emit two nested
llmspans for one provider request.Parent run span
span_attributes.type = "task"Cursor Agentinput: the initial user prompt/message that started this runoutput: the final response after the agent/tool loop completesmetrics: aggregate canonical token metrics across child LLM calls when availableerrorfieldDo not add spans for individual assistant/status/request/usage stream events unless an event corresponds to a model call or actual tool execution required by the spec. Stream events should be used to reconstruct the specified parent/child spans, not produce an event-shaped trace tree.
Child model-call spans and leaf ownership
For each model call/turn, choose the span type from the VCR-observed hierarchy:
llmleaf: emit a Cursorllmspan with the messages sent for that call, the complete response, resolvedmetadata.model,metadata.provider = "cursor", allowedmetadata.tools, and canonical per-call token metrics.llmleaf exists: emit the Cursor model-turn wrapper astask, keep model/provider attribution when available, and let the lower-level providerllmspan own canonical LLM input/output, token metrics, streaming metrics, and cost attribution. Do not copy those metrics onto the intermediate Cursor task.The lower-level leaf must retain its own provider integration origin (for example,
openai-auto); Cursor instrumentation must not overwrite it withcursor-sdk-auto.Cursor does not have a dedicated provider-native payload exception in the specification, so any Cursor-owned
llminput/output should use the canonical OpenAI Chat Completions shape. If the SDK/bridge does not expose enough information to identify model-call boundaries or reconstruct a compliant Cursor-owned leaf, investigate lower-level bridge events. Do not create a token-less Cursorllmspan above another providerllmspan.Child tool spans
Each actual model-initiated tool execution must produce exactly one
toolchild span:span_attributes.name: tool/function nameinput: model-provided tool argumentsoutput: tool return valueerrormetadata.tool_approval: onlyapprovedordenied, and only when Cursor exposes a reliable approval/denial signal for that exact interactionCorrelate Cursor's repeated
SDKToolUseMessagestart/completion/error events internally so they create one span rather than one span per event. This applies to built-in, MCP, subagent/delegation, shell, and PythonCustomTool.executecalls when the SDK exposes actual execution boundaries. Preserve custom-tool return values, exceptions, and Braintrust context.Do not infer approval from a missing completion event, and do not use approval metadata to represent execution success/failure.
Payload, metrics, and streaming requirements
Token metrics
Use only canonical Braintrust metric names and omit values Cursor did not report; do not fabricate zeroes. Based on Cursor's documented
TokenUsagesemantics, verify and map per-turn usage as follows:prompt_tokens = input_tokens + cache_read_tokens + cache_write_tokenscompletion_tokens = output_tokenstokens = prompt_tokens + completion_tokensprompt_cached_tokens = cache_read_tokensprompt_cache_creation_tokens = cache_write_tokenscompletion_reasoning_tokens = reasoning_tokenswhen reportedAll token counts must be non-negative integers. Cache-read and cache-write counts are subsets of
prompt_tokens, not additional tokens beyond it. Apply this per-turn mapping only when Cursor owns thellmleaf. If a lower-level providerllmspan exists, that leaf owns per-call token and cost telemetry; the intermediate Cursor model-turntaskmust not duplicate it. The top-level Cursor runtaskmay still aggregate run usage for display. Do not callagent.get_usage()solely to enrich traces or emit cost data that was not returned by the observed run.For streaming spans, measure
time_to_first_tokenin seconds from request start to the first generated chunk. Usestart/endfor timing; do not inventduration_msor other metric keys.Streaming behavior
Streaming must accumulate into the same complete span shape as non-streaming execution while preserving Cursor's one-shot stream semantics. Instrumentation must not eagerly consume, double-drain, buffer in a way that changes backpressure, or change sync/async iterator and return types.
A run exposes several mutually consuming interfaces (
messages,events, direct iteration,iter_text,text, andwait). Whichever path the user chooses must finalize each run/model/tool span exactly once on normal completion, error, cancellation, or expiration.User-provided
on_deltaandon_stepcallbacks must be preserved. Sync callbacks run inline; async callbacks may be sync or async, and awaitable returns must still be awaited before the next event is processed.Multimodal input
For
UserMessageimages:inputwith Braintrust attachment references at the original media leafimage_url.urlplacement for normalized image contentAPI keys, bridge bearer tokens, environment-variable values, MCP authorization headers, and other credentials must never be captured.
Reasoning
Capture provider-surfaced reasoning summaries using the specification's reasoning output structure and map reported reasoning usage to
completion_reasoning_tokens. Include prior reasoning context in subsequent model-call inputs when Cursor exposes it. Do not invent a Cursor-specific reasoning metric or capture additional hidden chain-of-thought fields outside the specification.Metadata and provenance
Only emit metadata allowed by the instrumentation specification. In particular, do not emit ad hoc
cursor_sdk.agent_id,cursor_sdk.run_id, runtime, status, duration, repository, branch, PR, or mode metadata without first adding it tobraintrust-spec.Every integration-created span must include stable origin provenance with:
Implementation parallels with Claude Agent SDK
Cursor SDK support can likely follow the existing integrations architecture:
CursorSDKIntegrationbuilt onBaseIntegrationsetup_cursor_sdk()entry point plusauto_instrument()support[tool.braintrust.matrix], a dedicated nox session, and versioned recordingsDifferences to account for:
Agent/AsyncAgent,Run/AsyncRun,CursorClient/AsyncClient)Testing / acceptance criteria
llmleaf or ataskabove an existing providerllmleaf; never allow nestedllmspans for one requestAgent.create().send().messages()andAsyncAgent.create().send().messages()Agent.prompt()/AsyncAgent.prompt()Agent.resume().send().wait()and explicitclient.agents.*flowsmessages,events, direct iteration,iter_text,text,wait,observe) finalizes spans once and preserves return/iterator typesllminput/output,metadata.model,metadata.provider = "cursor", and canonical per-turn token metricstask→ providerllm, with leaf-owned metrics/provider provenance and no duplicated intermediate LLM telemetryon_deltaandon_step, including awaited async callbacksCustomTool.executetracingtime_to_first_tokenfor streaming and reasoning/cache metric mappingcontext.span_origin.instrumentation.name == "cursor-sdk-auto"on every integration-created span without leaking that origin onto user-created nested spansauto_instrument()coverageUpstream references
@cursor/sdk) not instrumented — add wrapper and auto-instrumentation for Cursor agents braintrust-sdk-javascript#1919Local files inspected
py/src/braintrust/integrations/— no Cursor SDK integrationpy/src/braintrust/wrappers/— no Cursor SDK compatibility wrapperpy/src/braintrust/auto.py— no Cursor SDK auto-instrumentationpy/pyproject.toml— nocursor-sdkmatrix or cassette mappingpy/noxfile.py— no Cursor SDK test sessionpy/— no Cursor SDK-specific matches