mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 20:42:10 +00:00
feat: Add experimental session delete support (#682)
- **Update Agent Client Protocol SDK to 0.22.0** - **feat: Add session delete support**
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.35.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.21.1",
|
||||
"@agentclientprotocol/sdk": "0.22.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.143",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.21.1.tgz",
|
||||
"integrity": "sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==",
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.0.tgz",
|
||||
"integrity": "sha512-mnkpiuEfGm7/snhWbrRhe3Hk+WDDjv7U2N/Ql0XiThl9zg7T984dPH8Tich4wlBQnaaXTV4Fl5OQSe2pa9hoFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.21.1",
|
||||
"@agentclientprotocol/sdk": "0.22.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.143",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
SetSessionModeResponse,
|
||||
CloseSessionRequest,
|
||||
CloseSessionResponse,
|
||||
DeleteSessionRequest,
|
||||
DeleteSessionResponse,
|
||||
TerminalHandle,
|
||||
TerminalOutputResponse,
|
||||
WriteTextFileRequest,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import {
|
||||
CanUseTool,
|
||||
deleteSession,
|
||||
getSessionMessages,
|
||||
listSessions,
|
||||
McpServerConfig,
|
||||
@@ -629,6 +632,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
list: {},
|
||||
resume: {},
|
||||
close: {},
|
||||
delete: {},
|
||||
},
|
||||
},
|
||||
agentInfo: {
|
||||
@@ -1297,6 +1301,16 @@ export class ClaudeAcpAgent implements Agent {
|
||||
return {};
|
||||
}
|
||||
|
||||
async unstable_deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
||||
// Tear down any active in-memory state first so the on-disk file isn't
|
||||
// recreated by an outstanding query writing to it.
|
||||
if (this.sessions[params.sessionId]) {
|
||||
await this.teardownSession(params.sessionId);
|
||||
}
|
||||
await deleteSession(params.sessionId);
|
||||
return {};
|
||||
}
|
||||
|
||||
async unstable_setSessionModel(
|
||||
params: SetSessionModelRequest,
|
||||
): Promise<SetSessionModelResponse | void> {
|
||||
@@ -1834,7 +1848,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
} else if (!("type" in server)) {
|
||||
// Stdio type MCP server (with or without explicit type field)
|
||||
mcpServers[server.name] = {
|
||||
type: "stdio",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from "vitest";
|
||||
import { spawn, spawnSync } from "child_process";
|
||||
import {
|
||||
Agent,
|
||||
@@ -35,8 +35,16 @@ import {
|
||||
type SDKMessageFilter,
|
||||
} from "../acp-agent.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { deleteSession, query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
return {
|
||||
...actual,
|
||||
deleteSession: vi.fn(),
|
||||
};
|
||||
});
|
||||
import type {
|
||||
BetaToolResultBlockParam,
|
||||
BetaToolSearchToolResultBlockParam,
|
||||
@@ -1888,6 +1896,94 @@ describe("session/close", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/delete", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
} as unknown as AgentSideConnection;
|
||||
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
}
|
||||
|
||||
function injectSession(agent: ClaudeAcpAgent, sessionId: string) {
|
||||
function* empty() {}
|
||||
const gen = Object.assign(empty(), { interrupt: vi.fn(), close: vi.fn() });
|
||||
agent.sessions[sessionId] = {
|
||||
query: gen as any,
|
||||
input: new Pushable(),
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
modelInfos: [],
|
||||
settingsManager: { dispose: vi.fn() } as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cachedReadTokens: 0,
|
||||
cachedWriteTokens: 0,
|
||||
},
|
||||
configOptions: [],
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
contextWindowSize: 200000,
|
||||
taskState: new Map(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(deleteSession).mockReset();
|
||||
vi.mocked(deleteSession).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("tears down the active session and deletes it from disk", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "session-1");
|
||||
|
||||
const result = await agent.unstable_deleteSession({ sessionId: "session-1" });
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(agent.sessions["session-1"]).toBeUndefined();
|
||||
expect(session.query.interrupt).toHaveBeenCalled();
|
||||
expect(session.settingsManager.dispose).toHaveBeenCalled();
|
||||
expect(session.abortController.signal.aborted).toBe(true);
|
||||
expect(deleteSession).toHaveBeenCalledWith("session-1");
|
||||
});
|
||||
|
||||
it("deletes a session from disk that is not currently active", async () => {
|
||||
const agent = createMockAgent();
|
||||
|
||||
const result = await agent.unstable_deleteSession({ sessionId: "not-active" });
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(deleteSession).toHaveBeenCalledWith("not-active");
|
||||
});
|
||||
|
||||
it("propagates errors from the SDK delete call", async () => {
|
||||
const agent = createMockAgent();
|
||||
vi.mocked(deleteSession).mockRejectedValueOnce(new Error("Session not found on disk"));
|
||||
|
||||
await expect(agent.unstable_deleteSession({ sessionId: "missing" })).rejects.toThrow(
|
||||
"Session not found on disk",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not affect other sessions when deleting one", async () => {
|
||||
const agent = createMockAgent();
|
||||
injectSession(agent, "session-a");
|
||||
injectSession(agent, "session-b");
|
||||
|
||||
await agent.unstable_deleteSession({ sessionId: "session-a" });
|
||||
|
||||
expect(agent.sessions["session-a"]).toBeUndefined();
|
||||
expect(agent.sessions["session-b"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateSession param change detection", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
|
||||
Reference in New Issue
Block a user