Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/provider-cursor/src/server/CursorAgentSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,34 @@ describe("CursorAgentSdkRunner", () => {
assert.equal(cursorSdkMock.runWait.mock.calls.length, 1);
}).pipe(Effect.provide(layerTest)),
);

it("names the SDK's reason, code and HTTP status in a runner failure's text", () => {
const failed = (cause: unknown) =>
CursorAgentSdk.cursorSdkFailureMessage(
new CursorAgentSdk.CursorAgentSdkRunnerError({ method: "agent.create", cause }),
);
assert.equal(
new CursorAgentSdk.CursorAgentSdkRunnerError({
method: "agent.create",
cause: new Error("Too many requests"),
}).message,
"Cursor Agent SDK agent.create failed.",
);
assert.equal(
failed(Object.assign(new Error("Too many requests"), { code: "rate_limited", status: 429 })),
"Cursor Agent SDK agent.create failed: Too many requests (rate_limited, HTTP 429)",
);
// Cursor's messages often start with the code already.
assert.equal(
failed(
Object.assign(new Error("[resource_exhausted] Error"), { code: "resource_exhausted" }),
),
"Cursor Agent SDK agent.create failed: [resource_exhausted] Error",
);
assert.equal(
failed(Object.assign(new Error("Invalid User API Key"), { code: "error", status: 401 })),
"Cursor Agent SDK agent.create failed: Invalid User API Key (HTTP 401)",
);
assert.equal(failed(undefined), "Cursor Agent SDK agent.create failed.");
});
});
29 changes: 28 additions & 1 deletion packages/provider-cursor/src/server/CursorAgentSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,34 @@ export class CursorAgentSdkRunnerError extends Schema.TaggedError<CursorAgentSdk
}
}

const isCursorAgentSdkRunnerError = Schema.is(CursorAgentSdkRunnerError);
/** The failure text a user sees: the SDK's own reason, which the error's message leaves out. */
export function cursorSdkFailureMessage(error: CursorAgentSdkRunnerError): string {
const reason = sdkErrorReason(error.cause);
return reason === undefined
? error.message
: `Cursor Agent SDK ${error.method} failed: ${reason}`;
}

/** Codes Cursor sends when it has no specific one. */
const GENERIC_SDK_ERROR_CODES = new Set(["", "error", "unknown"]);

/** The SDK's own message, with the code and HTTP status its errors carry. */
function sdkErrorReason(cause: unknown): string | undefined {
if (!(cause instanceof Error)) return undefined;
const message = cause.message.trim();
if (message.length === 0) return undefined;
const code = Reflect.get(cause, "code");
const status = Reflect.get(cause, "status");
const details = [
...(typeof code === "string" && !GENERIC_SDK_ERROR_CODES.has(code) && !message.includes(code)
? [code]
: []),
...(typeof status === "number" ? [`HTTP ${status}`] : []),
];
return details.length === 0 ? message : `${message} (${details.join(", ")})`;
}

export const isCursorAgentSdkRunnerError = Schema.is(CursorAgentSdkRunnerError);

export interface CursorAgentSdkOpenInput {
readonly operation: "create" | "resume";
Expand Down
141 changes: 141 additions & 0 deletions packages/provider-cursor/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,147 @@ describe("CursorAdapterV2", () => {
),
);

it.effect.each([
{
reason: "the error Cursor's run ended with",
wait: Effect.succeed({
id: "native-cursor-run",
requestId: "native-request",
status: "error" as const,
error: {
message: "Model Blocked Please ask your admin to enable access to Claude Fable 5.",
},
model: { id: "claude-fable-5" },
durationMs: 1,
}),
failure: {
class: "provider_error",
message: "Model Blocked Please ask your admin to enable access to Claude Fable 5.",
},
},
{
reason: "the SDK error that broke the run",
wait: Effect.fail(
new CursorAgentSdk.CursorAgentSdkRunnerError({
method: "run.wait",
cause: Object.assign(new Error("Too many requests"), {
code: "resource_exhausted",
status: 429,
}),
}),
),
failure: {
class: "transport_error",
message:
"Cursor Agent SDK run.wait failed: Too many requests (resource_exhausted, HTTP 429)",
},
},
])("fails the turn with $reason", ({ wait, failure }) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-v2-failure-" });
const instanceId = ProviderInstanceId.make("cursor");
const threadId = ThreadId.make("cursor-failure-thread");
const modelSelection = { instanceId, model: "claude-fable-5" };
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: workspace,
});
const adapter = yield* makeCursorAdapterV2({
instanceId,
settings: yield* decodeCursorSettings({}),
environment: { HOME: workspace },
}).pipe(
Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
assertComplete: Effect.void,
open: () =>
Effect.succeed({
agentId: "native-cursor-failure",
listMessages: Effect.succeed([]),
close: Effect.void,
send: () =>
Effect.succeed({
agentId: "native-cursor-failure",
runId: "native-cursor-run",
wait,
cancel: Effect.void,
}),
}),
}),
Effect.provide(TestProviderHost.layer({ cwd: workspace })),
);
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("cursor-failure-session"),
modelSelection,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection,
runtimePolicy,
});
const now = yield* DateTime.now;
yield* runtime.startTurn({
threadId,
providerThread,
modelSelection,
runtimePolicy,
runId: RunId.make("cursor-failure-run"),
runOrdinal: 1,
providerTurnOrdinal: 1,
attemptId: RunAttemptId.make("cursor-failure-attempt"),
rootNodeId: NodeId.make("cursor-failure-root"),
appThread: {
id: threadId,
projectId: ProjectId.make("cursor-failure-project"),
createdBy: "user",
creationSource: "web",
title: "Cursor failure",
providerInstanceId: instanceId,
modelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
activeProviderThreadId: providerThread.id,
lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
forkedFrom: null,
createdAt: now,
updatedAt: now,
archivedAt: null,
settledOverride: null,
settledAt: null,
lastVisitedAt: null,
deletedAt: null,
},
message: {
messageId: MessageId.make("cursor-failure-message"),
createdBy: "user",
creationSource: "web",
text: "hi",
attachments: [],
},
});
const terminal = yield* runtime.events.pipe(
Stream.filter((event) => event.type === "turn.terminal"),
Stream.runHead,
);
assert.isTrue(Option.isSome(terminal));
if (Option.isSome(terminal)) {
assert.equal(terminal.value.status, "failed");
assert.equal(terminal.value.failure?.class, failure.class);
assert.equal(terminal.value.failure?.message, failure.message);
}
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(NodeServices.layer, IdAllocator.layer, McpProviderSessions.layer),
),
),
);

it.effect("fails standalone SDK transport diagnostics and sends compaction as /compress", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
28 changes: 19 additions & 9 deletions packages/provider-cursor/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2299,14 +2299,18 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
status,
...(status === "failed"
? {
failure: makeProviderFailure({
cause:
transportFailure ?? (result as { readonly error?: unknown }).error,
class:
transportFailure === undefined
? "provider_error"
: "transport_error",
}),
failure:
transportFailure === undefined
? makeProviderFailure({
cause: result.error,
// makeProviderFailure never reads a cause's own text.
message: result.error?.message,
class: "provider_error",
})
: makeProviderFailure({
cause: transportFailure,
class: "transport_error",
}),
}
: {}),
});
Expand All @@ -2322,7 +2326,13 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
...(context.interrupted
? {}
: {
failure: makeProviderFailure({ cause, class: "transport_error" }),
failure: makeProviderFailure({
cause,
message: CursorAgentSdk.isCursorAgentSdkRunnerError(cause)
? CursorAgentSdk.cursorSdkFailureMessage(cause)
: undefined,
class: "transport_error",
}),
}),
});
}
Expand Down
Loading