Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/AmpAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ it.effect("AmpAdapter delegates session startup to the manager", () =>
Effect.gen(function* () {
const manager = new FakeAmpManager();
const adapter = yield* makeAmpAdapter(enabledAmpSettings, { manager });
NodeAssert.equal(adapter.capabilities.supportsConversationRollback, false);

const session = yield* adapter.startSession({
threadId: asThreadId("thread-1"),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/AmpAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const makeAmpAdapter = Effect.fn("makeAmpAdapter")(function* (

const service: AmpAdapterShape = {
provider: PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession: (input) =>
Effect.gen(function* () {
if (!ampSettings.enabled) {
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/provider/Layers/CopilotAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ const modeLayer = it.layer(
);

modeLayer("CopilotAdapterLive interaction mode", (it) => {
it.effect("declares conversation rollback unsupported", () =>
Effect.gen(function* () {
const adapter = yield* CopilotAdapter;
NodeAssert.equal(adapter.capabilities.supportsConversationRollback, false);
}),
);
// Skip: @github/copilot-sdk has broken ESM resolution (vscode-jsonrpc/node) in CI
it.effect.skip("switches the Copilot session mode when interactionMode changes", () =>
Effect.gen(function* () {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/CopilotAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1743,7 +1743,7 @@ export const makeCopilotAdapter = Effect.fn("makeCopilotAdapter")(function* (

return {
provider: PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession,
sendTurn,
interruptTurn,
Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,32 @@ async function withAdapter<A>(
}

describe("CursorAdapter SDK", () => {
it("rejects rollback without changing the retained conversation", async () => {
const run = new FakeRun("run-rollback", "agent-rollback", [
makeSdkMessage({
type: "assistant",
message: { role: "assistant", content: [{ type: "text", text: "done" }] },
}),
]);
const fakeClient = new FakeCursorSdkClient(new FakeAgent("agent-rollback", run));
await withAdapter(fakeClient, (adapter) =>
Effect.gen(function* () {
expect(adapter.capabilities.supportsConversationRollback).toBe(false);
const threadId = asThreadId("thread-rollback");
yield* adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
const events = yield* collectThroughTurnCompleted(adapter);
yield* adapter.sendTurn({ threadId, input: "hello", attachments: [] });
yield* Fiber.join(events);
const before = yield* adapter.readThread(threadId);
expect(before.turns).toHaveLength(1);
const rollback = yield* adapter.rollbackThread(threadId, 1).pipe(Effect.exit);
expect(rollback._tag).toBe("Failure");
expect(yield* adapter.readThread(threadId)).toEqual(before);
expect((yield* adapter.readThread(threadId)).turns).toHaveLength(1);
}),
);
});

it("creates local SDK agents, applies model params, and emits canonical runtime events", async () => {
const run = new FakeRun("run-1", "agent-1", [
makeSdkMessage({ type: "status", status: "RUNNING" }),
Expand Down
14 changes: 7 additions & 7 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1268,19 +1268,19 @@ export function makeCursorAdapter(

const rollbackThread: CursorAdapterShape["rollbackThread"] = (threadId, numTurns) =>
Effect.gen(function* () {
const context = yield* requireSession(threadId);
yield* requireSession(threadId);
if (!Number.isInteger(numTurns) || numTurns < 1) {
return yield* new ProviderAdapterValidationError({
provider: PROVIDER,
operation: "rollbackThread",
issue: "numTurns must be an integer >= 1.",
});
}
context.turns.splice(Math.max(0, context.turns.length - numTurns));
return {
threadId,
turns: context.turns,
};
return yield* new ProviderAdapterRequestError({
provider: PROVIDER,
method: "thread/rollback",
detail: "Cursor SDK does not support provider-side conversation rollback.",
});
});

const stopSession: CursorAdapterShape["stopSession"] = (threadId) =>
Expand Down Expand Up @@ -1317,7 +1317,7 @@ export function makeCursorAdapter(

return {
provider: PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession,
sendTurn,
interruptTurn,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/DroidAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@ it.effect("reads Droid thread snapshots and rejects unsupported rollback", () =>

const before = yield* adapter.readThread(threadId);
NodeAssert.equal(before.turns.length, 2);
NodeAssert.equal(adapter.capabilities.supportsConversationRollback, false);
const rollback = yield* adapter.rollbackThread(threadId, 1).pipe(Effect.exit);
NodeAssert.equal(rollback._tag, "Failure");
if (rollback._tag === "Failure") {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/DroidAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ export function makeDroidAdapter(settings: DroidSettings, options?: DroidAdapter

return {
provider: DROID_PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession,
sendTurn,
interruptTurn: (threadId) =>
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/GeminiCliAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ it.effect("delegates session startup to the manager", () =>
Effect.gen(function* () {
const manager = new FakeGeminiCliManager();
const adapter = yield* makeGeminiCliAdapter(enabledConfig, { manager });
NodeAssert.equal(adapter.capabilities.supportsConversationRollback, false);

const session = yield* adapter.startSession({
threadId: asThreadId("thread-1"),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/GeminiCliAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const makeGeminiCliAdapter = Effect.fn("makeGeminiCliAdapter")(function*

const adapter: GeminiCliAdapterShape = {
provider: PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession: (input) =>
Effect.gen(function* () {
if (!config.enabled) {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/StandardAcpAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2153,7 +2153,7 @@ export function makeStandardAcpAdapter<UserInputParams = never, UserInputEncoded

return {
provider: PROVIDER,
capabilities: { sessionModelSwitch: "in-session" },
capabilities: { sessionModelSwitch: "in-session", supportsConversationRollback: false },
startSession,
sendTurn,
interruptTurn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ it.layer(testLayer)("standard ACP provider adapters", (it) => {
: provider === "ohMyPi"
? decodeOhMyPiSettings({ binaryPath }).pipe(Effect.flatMap(makeOhMyPiAdapter))
: decodePiSettings({ binaryPath }).pipe(Effect.flatMap(makePiAdapter));
assert.strictEqual(adapter.capabilities.supportsConversationRollback, false);
const threadId = ThreadId.make(`${provider}-mock-thread`);
const providerKind = ProviderDriverKind.make(provider);
const events: ProviderRuntimeEvent[] = [];
Expand Down
Loading