Bind Cursor rich input and activity adapters to an active parent turn

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 15:16:00 -05:00
1 parent 72d7bbc9b8
commit e2000ca283
3 files changed
+136 -4

No files matched your search

@@ -4,7 +4,7 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA } from "../../contracts/question-set.js";
import {
createCursorNotificationNormalizer, createCursorSubagentNormalizer, cursorWorkspaceArtifactReference,
createCursorNotificationNormalizer, createCursorProfileExtensionAdapter, createCursorSubagentNormalizer, cursorWorkspaceArtifactReference,
normalizeCursorPlanRequest, normalizeCursorQuestionRequest, normalizeCursorSubagentUpdate,
} from "./cursor-extensions.js";
@@ -94,3 +94,53 @@ describe("Cursor rich notifications", () => {
expect(normalize({ ...base, sessionUpdate: "subagent_state_update", state: "completed" })).toMatchObject({ payload: { children: [{ role: "Explore", model: "small", summary: "Find tests", status: "completed" }] } });
});
});
describe("Cursor active-turn extension adapter", () => {
it("roundtrips canonical answers and plan decisions while rejecting implicit acceptance", async () => {
const adapter = createCursorProfileExtensionAdapter({ workspacePath: await workspace(), sessionId: "parent", turnId: "turn" });
const question = await adapter.request("cursor/ask_question", { sessionId: "parent", toolCallId: "ask", questions: [
{ id: "native", prompt: "Choose", options: [{ id: "native-option", label: "One" }] },
] });
if (!("input" in question)) throw new Error("question was not blocking");
expect(question.input.resolve({ action: "submit", response: response({ "question-1": { selectedOptionIds: ["option-1"] } }) })).toEqual({ outcome: { outcome: "answered", answers: [{ questionId: "native", selectedOptionIds: ["native-option"] }] } });
expect(question.input.resolve({ action: "decline" })).toEqual({ outcome: { outcome: "skipped" } });
expect(question.input.resolve({ action: "cancel" })).toEqual(question.input.cancel());
expect(() => question.input.resolve({ action: "accept" })).toThrow();
expect(() => question.input.resolve({ action: "submit", answers: { "question-1": { answers: ["option-1"] } } })).toThrow();
const plan = await adapter.request("cursor/create_plan", { sessionId: "parent", toolCallId: "plan", plan: "# Complete plan\n".repeat(1_000), todos: [] });
if (!("input" in plan)) throw new Error("plan was not blocking");
expect(plan.input.details.revision).toMatch(/^[a-f0-9]{64}$/);
const id = plan.input.questionSet.questions[0]!.id;
expect(plan.input.resolve({ action: "submit", response: response({ [id]: { selectedOptionIds: ["accept"] } }) })).toEqual({ outcome: { outcome: "accepted" } });
expect(() => plan.input.resolve({ action: "accept" })).toThrow("displayed revision");
expect(plan.input.resolve({ action: "decline" })).toEqual({ outcome: { outcome: "rejected" } });
expect(plan.input.cancel()).toEqual({ outcome: { outcome: "cancelled" } });
});
it("acknowledges native request-shaped activity and preserves its reducer order", async () => {
const adapter = createCursorProfileExtensionAdapter({ workspacePath: await workspace(), sessionId: "parent", turnId: "turn" });
const first = await adapter.request("cursor/update_todos", { sessionId: "parent", toolCallId: "a", merge: false, todos: [{ id: "a", content: "Read", status: "pending" }] });
expect(first).toMatchObject({ response: {}, events: [{ eventType: "plan.updated", payload: { revision: 1 } }] });
const second = await adapter.notification("cursor/update_todos", { sessionId: "parent", toolCallId: "b", merge: true, todos: [{ id: "b", content: "Write", status: "pending" }] });
expect(second).toMatchObject([{ payload: { revision: 2, steps: [{ body: "Read" }, { body: "Write" }] } }]);
await expect(adapter.request("cursor/update_todos", { sessionId: "other" })).rejects.toThrow("stale parent");
await expect(adapter.notification("cursor/task", { sessionId: "other" })).rejects.toThrow("stale parent");
await expect(adapter.request("cursor/unknown", { sessionId: "parent" })).rejects.toThrow("Unsupported");
});
it("requires a known child spawn and immutable origin within the active parent", async () => {
const context = { workspacePath: await workspace(), sessionId: "parent", turnId: "turn" };
const adapter = createCursorProfileExtensionAdapter(context);
const update = { subagentSessionId: "child", _meta: { cursor: { toolCallId: "task", agentId: "native-child", model: "small" } } };
const state = { ...update, sessionUpdate: "subagent_state_update", state: "completed" };
await expect(adapter.notification("cursor/subagent_update", { sessionId: "parent", update: state })).rejects.toThrow("no spawn");
const spawned = { ...update, sessionUpdate: "subagent_spawned", name: "Research", task: "Inspect source" };
expect(await adapter.notification("cursor/subagent_update", { sessionId: "parent", update: spawned })).toMatchObject([{ eventType: "delegation.started", payload: { children: [{ role: "Research", model: "small" }] } }]);
expect(await adapter.notification("cursor/subagent_update", { sessionId: "parent", update: state })).toMatchObject([{ eventType: "delegation.completed", payload: { children: [{ role: "Research", summary: "Inspect source" }] } }]);
await expect(adapter.notification("cursor/subagent_update", { sessionId: "other", update: state })).rejects.toThrow("stale parent");
await expect(adapter.notification("cursor/subagent_update", { sessionId: "parent", update: { ...state, _meta: { cursor: { ...update._meta.cursor, toolCallId: "forged" } } } })).rejects.toThrow("origin identity");
await expect(adapter.notification("cursor/subagent_update", { sessionId: "parent", update: spawned })).rejects.toThrow("duplicated");
await expect(createCursorProfileExtensionAdapter({ ...context, turnId: "next" }).notification("cursor/subagent_update", { sessionId: "parent", update: state })).rejects.toThrow("no spawn");
});
});
@@ -9,6 +9,7 @@ import {
type PaperclipQuestionSet,
} from "../../contracts/question-set.js";
import type { CanonicalProviderEvent } from "../../provider-events.js";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
export const CURSOR_EXTENSION_REQUEST_METHODS = [
"cursor/ask_question", "cursor/create_plan",
@@ -18,6 +19,85 @@ export const CURSOR_EXTENSION_REQUEST_METHODS = [
export const CURSOR_EXTENSION_NOTIFICATION_METHODS = [
"cursor/update_todos", "cursor/task", "cursor/generate_image",
] as const;
export const CURSOR_CLIENT_CAPABILITIES = { _meta: { subagents: true } } as const;
type CursorExtensionInput = {
method: string;
questionSet: PaperclipQuestionSet;
details: Record<string, unknown>;
resolve(resolution: HarnessRuntimeRequestResolution): Record<string, unknown>;
cancel(): Record<string, unknown>;
};
type CursorExtensionResult = { input: CursorExtensionInput }
| { events: CanonicalProviderEvent[]; response: Record<string, unknown> };
/** One adapter belongs to exactly one active session/turn, never a warm process. */
export function createCursorProfileExtensionAdapter(context: {
workspacePath: string; sessionId: string; turnId: string;
}) {
const normalizeNotification = createCursorNotificationNormalizer(context);
const normalizeSubagent = createCursorSubagentNormalizer();
const children = new Map<string, { toolCallId: string; agentId?: string }>();
const assertSession = (params: Record<string, unknown>) => {
if (params.sessionId !== context.sessionId) throw new Error("Cursor extension has a stale parent session");
};
return {
async request(method: string, params: Record<string, unknown>): Promise<CursorExtensionResult> {
assertSession(params);
if (method === "cursor/ask_question") {
const question = normalizeCursorQuestionRequest(params);
return { input: {
method, questionSet: question.questionSet, details: { toolCallId: question.toolCallId },
cancel: question.cancel,
resolve(resolution) {
if (resolution.action === "cancel") return question.cancel();
if (resolution.action === "decline") return question.skip();
if (resolution.action !== "submit" || !("response" in resolution)) throw new Error("Cursor questions require a complete canonical answer");
return question.resolve(resolution.response);
},
} };
}
if (method === "cursor/create_plan") {
const plan = normalizeCursorPlanRequest(params);
return { input: {
method, questionSet: plan.questionSet, details: { toolCallId: plan.toolCallId, revision: plan.revision },
cancel: plan.cancel,
resolve(resolution) {
if (resolution.action === "cancel") return plan.cancel();
if (resolution.action === "decline") return { outcome: { outcome: "rejected" } };
if (resolution.action !== "submit" || !("response" in resolution)) throw new Error("Cursor plan decisions require the displayed revision");
return plan.resolve(resolution.response);
},
} };
}
if (!(CURSOR_EXTENSION_NOTIFICATION_METHODS as readonly string[]).includes(method)) throw new Error("Unsupported Cursor extension request");
return { events: await normalizeNotification(method, params), response: {} };
},
async notification(method: string, params: Record<string, unknown>): Promise<CanonicalProviderEvent[]> {
assertSession(params);
if (method !== "cursor/subagent_update") return normalizeNotification(method, params);
// The transport synthesizes this method only from the two pinned native
// session/update variants, after checking the active parent connection.
const update = object(params.update);
const id = requiredText(update.subagentSessionId, "subagentSessionId", 1_000);
if (id === context.sessionId) throw new Error("Cursor child cannot impersonate its parent session");
const metadata = object(object(update._meta).cursor);
const toolCallId = requiredText(metadata.toolCallId, "subagent toolCallId", 1_000);
const agentId = optionalText(metadata.agentId, "subagent agentId", 1_000);
const previous = children.get(id);
if (update.sessionUpdate === "subagent_spawned") {
if (previous || children.size >= 256) throw new Error("Cursor child session identity is duplicated or over capacity");
} else if (update.sessionUpdate !== "subagent_state_update" || !previous) {
throw new Error("Cursor child update has no spawn in this active turn");
}
if (previous && (previous.toolCallId !== toolCallId || previous.agentId !== agentId)) throw new Error("Cursor child update changed its origin identity");
const event = normalizeSubagent(update);
if (!event) throw new Error("Unsupported Cursor child update");
children.set(id, { toolCallId, agentId });
return [event];
},
};
}
type CursorQuestionResult = { outcome:
| { outcome: "answered"; answers: Array<{ questionId: string; selectedOptionIds: string[] }> }