Cursor SDK Bridge Issue: UnknownAgentError collapsed to generic internal: internal error due to incomplete error-name mapping

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

When the bridge subprocess catches a CursorSdkError whose class name isn’t in SDK_ERROR_NAME_TO_CONNECT_CODE (and whose .code isn’t in SDK_ERROR_CODE_TO_CONNECT_CODE), it returns the error untranslated. Connect-node’s default adapter then collapses it to Code.Internal, surfacing as internal: internal error in any non-JS bridge consumer. The real error class, message, and stack are unrecoverable from outside the bridge process — making certain failure modes (notably “agent already has active run”) opaque and unactionable for downstream clients like cursor-sdk Python.

The file responsible (cursor_sdk/_vendor/bridge/dist/sdk-error-interceptor.js) acknowledges this design dependency in its own comment:

“Without this translation, connect-node’s adapter collapses every non-ConnectError into Code.Internal, hiding the structured code from SDK consumers.”

…but the mapping table is incomplete.

Steps to Reproduce

Concretely: agent.send() throws UnknownAgentError: Agent <id> already has active run server-side. The Python client receives:

cursor_sdk.errors.InternalServerError: internal: internal error

The structured error name (UnknownAgentError), the real message (already has active run), the JS stack pointing inside StoreBackedAgent, and any .requestId/.helpUrl metadata are all lost.

Minimal Reproduction (Python, against the bridge)

"""
Reproduces: bridge collapses UnknownAgentError to "internal: internal error".

Triggers the active-run state by issuing a second `send()` before the first
run finishes, which causes `StoreBackedAgent` to throw `UnknownAgentError`
with the message "Agent <id> already has active run".

Usage:
  CURSOR_API_KEY=crsr_xxx uv run python repro.py
"""

import asyncio
import os

from cursor_sdk import AsyncAgent, AsyncClient
from cursor_sdk.types import AgentOptions, LocalAgentOptions


async def main() -> None:
    api_key = os.environ.get("CURSOR_API_KEY")
    if not api_key:
        raise SystemExit("CURSOR_API_KEY required")

    async with await AsyncClient.launch_bridge(
        workspace=os.getcwd(),
        allow_api_key_env_fallback=True,
    ) as client:
        async with await AsyncAgent.create(
            AgentOptions(
                model="composer-2.5",
                local=LocalAgentOptions(cwd=os.getcwd()),
            ),
            client=client,
        ) as agent:
            # Kick off a long-running first send but don't await its stream.
            run1 = await agent.send(
                "Read every file in this repo, then summarise in 5000 words."
            )
            # Give the bridge a moment to register the run as 'running'.
            await asyncio.sleep(0.5)

            # Second send while run1 is still in flight.
            try:
                await agent.send("ignore that, write hello world")
            except Exception as exc:
                print(f"caught: {type(exc).__name__}: {exc}")
                # Expected: AgentBusyError (or UnknownAgentError) with the
                #   real message "Agent <id> already has active run".
                # Actual:   InternalServerError: internal: internal error


if __name__ == "__main__":
    asyncio.run(main())

Reproduction Steps

pip install cursor-sdk
CURSOR_API_KEY=crsr_xxx python repro.py

Observe: the caught exception is InternalServerError: internal: internal error. The fact that the failure was due to an active run on this agent is not recoverable from the exception alone.

Expected Behavior

When the SDK throws an UnknownAgentError (or any structured CursorSdkError subclass) inside an RPC handler, the bridge should translate it to a meaningful Connect code so downstream clients receive the real error class, code, and message — not Code.Internal: internal: internal error.

Operating System

Linux

Version Information

  • cursor-sdk Python: v0.1.6
  • Bundled @cursor/sdk (Node, inside _vendor/bridge/node_modules/): v1.0.15
  • cursor-sdk-bridge server: ships with v0.1.6
  • Node: v22.x (bundled)
  • OS: Linux

Additional Information

Diagnostic Output (with bridge stderr patched to log raw errors)

caught: InternalServerError: internal: internal error

Inside the bridge process (only visible if you patch sdk-error-interceptor.js to log to stderr):

{
  "name": "UnknownAgentError",
  "message": "Agent agent-0c3974a4-... already has active run",
  "code": undefined,
  "stack": "UnknownAgentError: Agent agent-0c3974a4-... already has active run
    at g (.../node_modules/@cursor/sdk/dist/esm/index.js:8:1078869)
    at StoreBackedAgent.<anonymous> (.../node_modules/@cursor/sdk/dist/esm/index.js:8:6189282)"
}

Key observations:

  • The JS error has .name = "UnknownAgentError" but .code is undefined or unset, so neither lookup table in sdk-error-interceptor.js matches.
  • UnknownAgentError is also misleading as a class name — the message is about an active run, not an unknown agent. Semantically this is closer to AgentBusyError (which IS in the name map).
  • Because the error escapes the interceptor unchanged, connect-node collapses it to Code.Internal, and downstream clients lose the structured information.

Root Cause

cursor_sdk/_vendor/bridge/dist/sdk-error-interceptor.js:

const SDK_ERROR_NAME_TO_CONNECT_CODE = {
    AuthenticationError: Code.Unauthenticated,
    ConfigurationError: Code.InvalidArgument,
    AgentBusyError: Code.FailedPrecondition,
    RateLimitError: Code.ResourceExhausted,
};

Several CursorSdkError subclasses are absent from this map and from SDK_ERROR_CODE_TO_CONNECT_CODE:

  • UnknownAgentError
  • NotFoundError
  • BadRequestError
  • IntegrationNotConnectedError
  • InternalServerError
  • APITimeoutError
  • NetworkError
  • UnsupportedRunOperationError
  • PermissionDeniedError (inherits from AuthenticationError but error.name is "PermissionDeniedError", not "AuthenticationError", so it doesn’t match)

Anything thrown with one of those .name values and no recognised .code is silently collapsed.

Workaround

Patch the bridge’s sdk-error-interceptor.js post-install to add the missing entries:

const SDK_ERROR_NAME_TO_CONNECT_CODE = {
    AuthenticationError: Code.Unauthenticated,
    PermissionDeniedError: Code.PermissionDenied,
    ConfigurationError: Code.InvalidArgument,
    BadRequestError: Code.InvalidArgument,
    UnsupportedRunOperationError: Code.FailedPrecondition,
    AgentBusyError: Code.FailedPrecondition,
    IntegrationNotConnectedError: Code.FailedPrecondition,
    RateLimitError: Code.ResourceExhausted,
    NotFoundError: Code.NotFound,
    UnknownAgentError: Code.NotFound,            // or FailedPrecondition for the "active run" case
    APITimeoutError: Code.DeadlineExceeded,
    NetworkError: Code.Unavailable,
    InternalServerError: Code.Internal,          // preserves message instead of collapsing
};

This is brittle — it gets wiped on every pip install --upgrade cursor-sdk — but is the only way for non-JS bridge consumers to surface meaningful errors today.

Feature Request / Suggested Fix

One (or both) of:

  1. Add the missing entries to SDK_ERROR_NAME_TO_CONNECT_CODE upstream, with sensible Connect-code mappings. The list above is a reasonable starting point.

  2. Default fallback that preserves the error name/message rather than collapsing to Code.Internal: internal: internal error. e.g. if no mapping matches but the error is an instance of CursorSdkError, wrap it as new ConnectError(${error.name}: ${error.message}, Code.Internal, ...) so at least the class name and message survive.

  3. Audit UnknownAgentError usage — throwing it with the message “Agent X already has active run” is misleading; that condition is closer to AgentBusyError and would benefit from a dedicated class or .code value.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Thanks for the feedback @maxious!

We’re aware that the logging is quite opaque, and have a ticket open for this.