feat(acp): implemented multi-session support with forking and lifecycle management

- Added multi-session support to ACP mode enabling session forking, resumption, and closure operations.
- Added session model state reporting and unstable_setSessionModel RPC command for direct model configuration.
- Added turn-level usage tracking and stable message ID tracking for assistant chunks in ACP responses.
- Added Settings.cloneForCwd() method and ExtensionRunner.getFlagValues() for configuration isolation across sessions.
- Fixed ACP session cleanup to properly cancel in-flight prompts and dispose resources on disconnect.
- Refactored AcpAgent to manage multiple concurrent sessions with per-session lifecycle instead of single session.
This commit is contained in:
can1357
2026-04-08 06:54:33 +02:00
parent 58c0d52679
commit 53c8d33758
9 changed files with 1083 additions and 167 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from "bun:test";
import { mapAgentSessionEventToAcpSessionUpdates } from "../src/modes/acp/acp-event-mapper";
import type { AgentSessionEvent } from "../src/session/agent-session";
function makeAssistantMessage(text: string) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: "anthropic-messages" as const,
provider: "anthropic" as const,
model: "claude-sonnet-4-20250514",
usage: {
input: 10,
output: 5,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 15,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop" as const,
timestamp: Date.now(),
};
}
function getChunkMessageId(event: { update: object }): string | undefined {
const update = event.update as { messageId?: string | null };
return typeof update.messageId === "string" ? update.messageId : undefined;
}
describe("ACP event mapper", () => {
it("attaches a stable messageId to live assistant chunks", () => {
const assistantMessage = makeAssistantMessage("chunk");
const getMessageId = (message: unknown): string | undefined =>
message === assistantMessage ? "a80f1ff7-4f0a-4e6b-9f09-c94857b62a4a" : undefined;
const textUpdates = mapAgentSessionEventToAcpSessionUpdates(
{
type: "message_update",
message: assistantMessage,
assistantMessageEvent: { type: "text_delta", delta: "chunk" },
} as AgentSessionEvent,
"session-1",
{ getMessageId },
);
const thoughtUpdates = mapAgentSessionEventToAcpSessionUpdates(
{
type: "message_update",
message: assistantMessage,
assistantMessageEvent: { type: "thinking_delta", delta: "plan" },
} as AgentSessionEvent,
"session-1",
{ getMessageId },
);
expect(textUpdates).toHaveLength(1);
expect(thoughtUpdates).toHaveLength(1);
expect(textUpdates[0] ? getChunkMessageId(textUpdates[0]) : undefined).toBe(
"a80f1ff7-4f0a-4e6b-9f09-c94857b62a4a",
);
expect(thoughtUpdates[0] ? getChunkMessageId(thoughtUpdates[0]) : undefined).toBe(
"a80f1ff7-4f0a-4e6b-9f09-c94857b62a4a",
);
});
});