diff --git a/packages/server/agents.json b/packages/server/agents.json index 5a09cb1db..468314ba0 100644 --- a/packages/server/agents.json +++ b/packages/server/agents.json @@ -130,5 +130,77 @@ "createdAt": "2025-10-26T07:29:32.156Z", "lastActivityAt": "2025-10-26T07:29:40.533Z", "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-Gi719a" + }, + { + "id": "94e1e306-5837-4ce3-9a09-09f1c8ae2678", + "title": "Agent 94e1e306", + "sessionId": "0bc2cfed-8898-4834-8827-23283ba231cf", + "options": { + "type": "claude", + "sessionId": "0bc2cfed-8898-4834-8827-23283ba231cf" + }, + "createdAt": "2025-10-26T07:41:24.824Z", + "lastActivityAt": "2025-10-26T07:41:34.736Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-c77wpz" + }, + { + "id": "686a1db9-e41c-4d4b-b0ab-6ae7e573bbd3", + "title": "Agent 686a1db9", + "sessionId": "c62d4b37-df4b-4c73-8afb-9c8a994cd1c9", + "options": { + "type": "claude", + "sessionId": "c62d4b37-df4b-4c73-8afb-9c8a994cd1c9" + }, + "createdAt": "2025-10-26T07:42:30.564Z", + "lastActivityAt": "2025-10-26T07:42:39.893Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-m0yPgj" + }, + { + "id": "badd51fa-7f43-453a-802e-8504944cc388", + "title": "Agent badd51fa", + "sessionId": "09459e2b-92be-4535-9109-64f0f3eac5d5", + "options": { + "type": "claude", + "sessionId": "09459e2b-92be-4535-9109-64f0f3eac5d5" + }, + "createdAt": "2025-10-26T07:49:57.618Z", + "lastActivityAt": "2025-10-26T07:50:05.977Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-JNRc9I" + }, + { + "id": "26610309-c9e2-4a33-94c1-c4afe7ec439b", + "title": "Agent 26610309", + "sessionId": "a73bcefc-d829-4286-b192-a6a5c04927c0", + "options": { + "type": "claude", + "sessionId": "a73bcefc-d829-4286-b192-a6a5c04927c0" + }, + "createdAt": "2025-10-26T07:51:04.294Z", + "lastActivityAt": "2025-10-26T07:51:15.950Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-3t9vQe" + }, + { + "id": "4c4f71e5-cb60-4040-9911-746258b43d1f", + "title": "Agent 4c4f71e5", + "sessionId": "99af8673-ba9a-4fe1-bb96-19ea54760cfe", + "options": { + "type": "claude", + "sessionId": "99af8673-ba9a-4fe1-bb96-19ea54760cfe" + }, + "createdAt": "2025-10-26T07:52:50.829Z", + "lastActivityAt": "2025-10-26T07:53:02.229Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-UOhobf" + }, + { + "id": "835f1dc5-1398-4743-ad97-f9ebae232cc1", + "title": "Agent 835f1dc5", + "sessionId": "90231d13-4f40-435d-aec5-ce43d76ac98f", + "options": { + "type": "claude", + "sessionId": "90231d13-4f40-435d-aec5-ce43d76ac98f" + }, + "createdAt": "2025-10-26T07:57:16.797Z", + "lastActivityAt": "2025-10-26T07:57:29.704Z", + "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-GAWCrz" } ] \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index 3e7d5bb17..24bc71095 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,7 +19,7 @@ "dependencies": { "@agentclientprotocol/sdk": "^0.4.9", "@ai-sdk/openai": "^2.0.52", - "@boudra/claude-code-acp": "^0.8.3", + "@boudra/claude-code-acp": "^0.8.4", "@deepgram/sdk": "^3.4.0", "@modelcontextprotocol/sdk": "^1.20.1", "@openrouter/ai-sdk-provider": "^1.2.0", diff --git a/packages/server/src/server/acp/agent-manager.test.ts b/packages/server/src/server/acp/agent-manager.test.ts index 955f8cce3..61fafce61 100644 --- a/packages/server/src/server/acp/agent-manager.test.ts +++ b/packages/server/src/server/acp/agent-manager.test.ts @@ -19,85 +19,51 @@ describe("AgentManager", () => { await rm(tmpDir, { recursive: true, force: true }); }); - it( - "should create file after accepting plan", - async () => { - let permissionRequest: RequestPermissionRequest | null = null; - let requestId: string | null = null; - const testFile = join(tmpDir, "test.txt"); + it("should create file after accepting plan", async () => { + let permissionRequest: RequestPermissionRequest | null = null; + let requestId: string | null = null; + const testFile = join(tmpDir, "test.txt"); - const agentId = await manager.createAgent({ - cwd: tmpDir, - initialMode: "plan", - }); + const agentId = await manager.createAgent({ + cwd: tmpDir, + initialMode: "plan", + }); - const unsubscribe = manager.subscribeToUpdates( - agentId, - (update: AgentUpdate) => { - const notification: AgentNotification = update.notification; - if (notification.type === "permission") { - permissionRequest = notification.request; - requestId = notification.requestId; - } + const unsubscribe = manager.subscribeToUpdates( + agentId, + (update: AgentUpdate) => { + const notification: AgentNotification = update.notification; + if (notification.type === "permission") { + permissionRequest = notification.request; + requestId = notification.requestId; } + } + ); + + try { + await manager.sendPrompt( + agentId, + "Create a file called test.txt with the content 'hello world'" ); - try { - await manager.sendPrompt( - agentId, - "Create a file called test.txt with the content 'hello world'" - ); - - let attempts = 0; - while (!permissionRequest && attempts < 40) { - await new Promise((resolve) => setTimeout(resolve, 500)); - attempts++; - } - - expect(permissionRequest).toBeDefined(); - expect(requestId).toBeDefined(); - - const acceptOption = permissionRequest!.options.find( - (o) => o.kind === "allow_once" || o.kind === "allow_always" - ); - expect(acceptOption).toBeDefined(); - - manager.respondToPermission(agentId, requestId!, acceptOption!.optionId); - - let status = manager.getAgentStatus(agentId); - attempts = 0; - while (status !== "completed" && status !== "failed" && attempts < 60) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - status = manager.getAgentStatus(agentId); - attempts++; - } - - expect(status).toBe("completed"); - - const content = await readFile(testFile, "utf-8"); - expect(content).toContain("hello world"); - - const agentInfo = manager.listAgents().find((a) => a.id === agentId); - expect(agentInfo?.currentModeId).toBe("acceptEdits"); - } finally { - unsubscribe(); - await manager.killAgent(agentId); + let attempts = 0; + while (!permissionRequest && attempts < 40) { + await new Promise((resolve) => setTimeout(resolve, 500)); + attempts++; } - }, - 120000 - ); - it( - "should load persisted agent and send new prompt", - async () => { - const agentId = await manager.createAgent({ - cwd: tmpDir, - }); + expect(permissionRequest).toBeDefined(); + expect(requestId).toBeDefined(); - await manager.sendPrompt(agentId, "echo 'first message'"); + const acceptOption = permissionRequest!.options.find( + (o) => o.kind === "allow_once" || o.kind === "allow_always" + ); + expect(acceptOption).toBeDefined(); + + manager.respondToPermission(agentId, requestId!, acceptOption!.optionId); let status = manager.getAgentStatus(agentId); - let attempts = 0; + attempts = 0; while (status !== "completed" && status !== "failed" && attempts < 60) { await new Promise((resolve) => setTimeout(resolve, 1000)); status = manager.getAgentStatus(agentId); @@ -106,45 +72,82 @@ describe("AgentManager", () => { expect(status).toBe("completed"); - const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId); - const claudeSessionId = manager.getClaudeSessionId(agentId); - console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId); - console.log("Before kill - Claude Session ID:", claudeSessionId); - console.log("Working directory:", tmpDir); + const content = await readFile(testFile, "utf-8"); + expect(content).toContain("hello world"); + const agentInfo = manager.listAgents().find((a) => a.id === agentId); + expect(agentInfo?.currentModeId).toBe("acceptEdits"); + } finally { + unsubscribe(); await manager.killAgent(agentId); + } + }, 120000); - const newManager = new AgentManager(); - await newManager.initialize(); + it.only("should load persisted agent and send new prompt", async () => { + const agentId = await manager.createAgent({ + cwd: tmpDir, + }); - const agents = newManager.listAgents(); - const loadedAgent = agents.find((a) => a.id === agentId); - const loadedClaudeSessionId = newManager.getClaudeSessionId(agentId); + await manager.sendPrompt(agentId, "echo 'first message'"); - console.log("After reload - ACP Session ID:", loadedAgent?.sessionId); - console.log("After reload - Claude Session ID:", loadedClaudeSessionId); + let status = manager.getAgentStatus(agentId); + let attempts = 0; + while (status !== "completed" && status !== "failed" && attempts < 60) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + status = manager.getAgentStatus(agentId); + attempts++; + } - expect(loadedAgent).toBeDefined(); - expect(loadedAgent?.id).toBe(agentId); - expect(loadedAgent?.cwd).toBe(tmpDir); + expect(status).toBe("completed"); - await newManager.sendPrompt(agentId, "echo 'second message'"); + const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId); + const claudeSessionId = manager.getClaudeSessionId(agentId); + console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId); + console.log("Before kill - Claude Session ID:", claudeSessionId); + console.log("Working directory:", tmpDir); + await manager.killAgent(agentId); + + const newManager = new AgentManager(); + await newManager.initialize(); + + const agents = newManager.listAgents(); + const loadedAgent = agents.find((a) => a.id === agentId); + const loadedClaudeSessionId = newManager.getClaudeSessionId(agentId); + + console.log("After reload - ACP Session ID:", loadedAgent?.sessionId); + console.log("After reload - Claude Session ID:", loadedClaudeSessionId); + + expect(loadedAgent).toBeDefined(); + expect(loadedAgent?.id).toBe(agentId); + expect(loadedAgent?.cwd).toBe(tmpDir); + + await newManager.initializeAgentAndGetHistory(agentId); + await new Promise((resolve) => setTimeout(resolve, 2000)); + const persistedUpdates = newManager.getAgentUpdates(agentId); + + console.log( + "Persisted updates:", + JSON.stringify(persistedUpdates, null, 2) + ); + + await newManager.sendPrompt(agentId, "echo 'second message'"); + + status = newManager.getAgentStatus(agentId); + attempts = 0; + while (status !== "completed" && status !== "failed" && attempts < 60) { + await new Promise((resolve) => setTimeout(resolve, 1000)); status = newManager.getAgentStatus(agentId); - attempts = 0; - while (status !== "completed" && status !== "failed" && attempts < 60) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - status = newManager.getAgentStatus(agentId); - attempts++; - } + attempts++; + } - expect(status).toBe("completed"); + expect(status).toBe("completed"); - const finalUpdates = newManager.getAgentUpdates(agentId); - expect(finalUpdates.length).toBeGreaterThan(0); + const finalUpdates = newManager.getAgentUpdates(agentId); + expect(finalUpdates.length).toBeGreaterThan(0); - await newManager.killAgent(agentId); - }, - 120000 - ); + console.log("Final updates:", finalUpdates); + + await newManager.killAgent(agentId); + }, 120000); }); diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts index 9ebf5584d..9b1e133d6 100644 --- a/packages/server/src/server/acp/agent-manager.ts +++ b/packages/server/src/server/acp/agent-manager.ts @@ -62,9 +62,7 @@ interface ManagedAgent { type UpdateAgentCallback = ( agentId: string, - updateFn: ( - agent: ManagedAgent - ) => boolean | void | Promise, + updateFn: (agent: ManagedAgent) => boolean | void | Promise, options?: { sessionId: string | null } ) => Promise; @@ -113,7 +111,10 @@ class ACPClient implements Client { async sessionUpdate(params: SessionNotification): Promise { if (params.update.sessionUpdate === "agent_message_chunk") { - console.log(`[Agent ${this.agentId}] Message chunk update:`, JSON.stringify(params, null, 2)); + console.log( + `[Agent ${this.agentId}] Message chunk update:`, + JSON.stringify(params, null, 2) + ); } // Check if this update contains a Claude session ID @@ -349,13 +350,15 @@ export class AgentManager { } // Convert prompt to ContentBlock array if it's a string - const contentBlocks: ContentBlock[] = typeof prompt === "string" - ? [{ type: "text", text: prompt }] - : prompt; + const contentBlocks: ContentBlock[] = + typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt; // Extract text for user message notification (use first text block) - const firstTextBlock = contentBlocks.find(block => block.type === "text"); - const userMessageText = firstTextBlock && "text" in firstTextBlock ? firstTextBlock.text : "[message with attachments]"; + const firstTextBlock = contentBlocks.find((block) => block.type === "text"); + const userMessageText = + firstTextBlock && "text" in firstTextBlock + ? firstTextBlock.text + : "[message with attachments]"; // Emit user message notification const userMessageUpdate: AgentUpdate = { @@ -540,6 +543,7 @@ export class AgentManager { if (runtime) { setTimeout(() => { if (!runtime.process.killed) { + console.log(`[Agent ${agentId}] Force killing process`); runtime.process.kill("SIGKILL"); } }, 5000); @@ -898,9 +902,7 @@ export class AgentManager { private async updateAgent( agentId: string, - updateFn: ( - agent: ManagedAgent - ) => boolean | void | Promise, + updateFn: (agent: ManagedAgent) => boolean | void | Promise, options?: { sessionId: string | null } ): Promise { const agent = this.agents.get(agentId); @@ -1185,7 +1187,9 @@ export class AgentManager { const runtime = this.getRuntime(agent); if (runtime && runtime.currentModeId !== newModeId) { - console.log(`[Agent ${agentId}] Mode changed: ${runtime.currentModeId} -> ${newModeId}`); + console.log( + `[Agent ${agentId}] Mode changed: ${runtime.currentModeId} -> ${newModeId}` + ); runtime.currentModeId = newModeId; } }