mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
Add session/close support (#409)
~Waiting for https://github.com/agentclientprotocol/agent-client-protocol/pull/723.~ --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.21.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.15.0",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.71",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
@@ -33,9 +33,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@agentclientprotocol/sdk": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.15.0.tgz",
|
||||
"integrity": "sha512-TH4utu23Ix8ec34srBHmDD4p3HI0cYleS1jN9lghRczPfhFlMBNrQgZWeBBe12DWy27L11eIrtciY2MXFSEiDg==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz",
|
||||
"integrity": "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==",
|
||||
"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.15.0",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.71",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
CloseSessionRequest,
|
||||
CloseSessionResponse,
|
||||
TerminalHandle,
|
||||
TerminalOutputResponse,
|
||||
WriteTextFileRequest,
|
||||
@@ -117,6 +119,7 @@ type Session = {
|
||||
promptRunning: boolean;
|
||||
pendingMessages: Map<string, { resolve: (cancelled: boolean) => void; order: number }>;
|
||||
nextPendingOrder: number;
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
type BackgroundTerminal =
|
||||
@@ -338,6 +341,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
fork: {},
|
||||
list: {},
|
||||
resume: {},
|
||||
close: {},
|
||||
},
|
||||
},
|
||||
agentInfo: {
|
||||
@@ -844,6 +848,19 @@ export class ClaudeAcpAgent implements Agent {
|
||||
await session.query.interrupt();
|
||||
}
|
||||
|
||||
async unstable_sessionClose(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
||||
const session = this.sessions[params.sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
await this.cancel({ sessionId: params.sessionId });
|
||||
|
||||
session.abortController.abort();
|
||||
delete this.sessions[params.sessionId];
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async unstable_setSessionModel(
|
||||
params: SetSessionModelRequest,
|
||||
): Promise<SetSessionModelResponse | void> {
|
||||
@@ -871,6 +888,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
if (typeof params.value !== "string") {
|
||||
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
||||
}
|
||||
|
||||
const option = session.configOptions.find((o) => o.id === params.configId);
|
||||
if (!option) {
|
||||
@@ -900,7 +920,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === params.configId ? { ...o, currentValue: params.value } : o,
|
||||
o.id === params.configId && typeof o.currentValue === "string"
|
||||
? { ...o, currentValue: params.value }
|
||||
: o,
|
||||
);
|
||||
|
||||
return { configOptions: session.configOptions };
|
||||
@@ -1125,7 +1147,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
if (!session) return;
|
||||
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId ? { ...o, currentValue: value } : o,
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
|
||||
await this.client.sessionUpdate({
|
||||
@@ -1220,6 +1242,8 @@ export class ClaudeAcpAgent implements Agent {
|
||||
userProvidedOptions?.tools ??
|
||||
(params._meta?.disableBuiltInTools === true ? [] : { type: "preset", preset: "claude_code" });
|
||||
|
||||
const abortController = userProvidedOptions?.abortController || new AbortController();
|
||||
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
@@ -1280,6 +1304,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
],
|
||||
},
|
||||
...creationOpts,
|
||||
abortController,
|
||||
};
|
||||
|
||||
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
||||
@@ -1288,7 +1313,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
// Handle abort controller from meta options
|
||||
const abortController = userProvidedOptions?.abortController;
|
||||
if (abortController?.signal.aborted) {
|
||||
throw new Error("Cancelled");
|
||||
}
|
||||
@@ -1376,6 +1400,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
||||
import { spawn, spawnSync } from "child_process";
|
||||
import {
|
||||
Agent,
|
||||
@@ -1328,6 +1328,7 @@ describe("stop reason propagation", () => {
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController: new AbortController(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1416,3 +1417,80 @@ describe("stop reason propagation", () => {
|
||||
).rejects.toThrow("Internal error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/close", () => {
|
||||
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() });
|
||||
agent.sessions[sessionId] = {
|
||||
query: gen as any,
|
||||
input: new Pushable(),
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
permissionMode: "default",
|
||||
settingsManager: {} as any,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cachedReadTokens: 0,
|
||||
cachedWriteTokens: 0,
|
||||
},
|
||||
configOptions: [],
|
||||
promptRunning: false,
|
||||
pendingMessages: new Map(),
|
||||
nextPendingOrder: 0,
|
||||
abortController: new AbortController(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
|
||||
it("should close an existing session and remove it", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "session-1");
|
||||
|
||||
expect(agent.sessions["session-1"]).toBeDefined();
|
||||
|
||||
const result = await agent.unstable_sessionClose({ sessionId: "session-1" });
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(agent.sessions["session-1"]).toBeUndefined();
|
||||
expect(session.query.interrupt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should abort the session's abort controller", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "session-2");
|
||||
|
||||
expect(session.abortController.signal.aborted).toBe(false);
|
||||
|
||||
await agent.unstable_sessionClose({ sessionId: "session-2" });
|
||||
|
||||
expect(session.abortController.signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw when closing a non-existent session", async () => {
|
||||
const agent = createMockAgent();
|
||||
|
||||
await expect(agent.unstable_sessionClose({ sessionId: "non-existent" })).rejects.toThrow(
|
||||
"Session not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not affect other sessions when closing one", async () => {
|
||||
const agent = createMockAgent();
|
||||
injectSession(agent, "session-a");
|
||||
injectSession(agent, "session-b");
|
||||
|
||||
await agent.unstable_sessionClose({ sessionId: "session-a" });
|
||||
|
||||
expect(agent.sessions["session-a"]).toBeUndefined();
|
||||
expect(agent.sessions["session-b"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user