From 1150158d557512d0c36f6b9f919ac5f9fbe230df Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 12 Jan 2026 16:06:14 -0800 Subject: [PATCH] Fix model switching --- apps/cli/src/acp/__tests__/agent.test.ts | 5 +- .../src/acp/__tests__/model-service.test.ts | 88 ++--- apps/cli/src/acp/__tests__/session.test.ts | 135 +++---- apps/cli/src/acp/agent.ts | 356 +++++++----------- apps/cli/src/acp/command-stream.ts | 33 +- apps/cli/src/acp/index.ts | 186 +-------- apps/cli/src/acp/model-service.ts | 139 ++----- apps/cli/src/acp/session-event-handler.ts | 15 +- apps/cli/src/acp/session.ts | 106 ++---- apps/cli/src/acp/types.ts | 94 ++--- apps/cli/src/commands/acp/index.ts | 22 +- apps/cli/src/types/constants.ts | 2 +- 12 files changed, 394 insertions(+), 787 deletions(-) diff --git a/apps/cli/src/acp/__tests__/agent.test.ts b/apps/cli/src/acp/__tests__/agent.test.ts index a682a261b4..64d01f59ba 100644 --- a/apps/cli/src/acp/__tests__/agent.test.ts +++ b/apps/cli/src/acp/__tests__/agent.test.ts @@ -1,6 +1,7 @@ import type * as acp from "@agentclientprotocol/sdk" -import { RooCodeAgent, type RooCodeAgentOptions } from "../agent.js" +import { RooCodeAgent } from "../agent.js" +import type { AcpSessionOptions } from "../session.js" vi.mock("@/commands/auth/index.js", () => ({ login: vi.fn().mockResolvedValue({ success: true }), @@ -24,7 +25,7 @@ describe("RooCodeAgent", () => { let agent: RooCodeAgent let mockConnection: acp.AgentSideConnection - const defaultOptions: RooCodeAgentOptions = { + const defaultOptions: AcpSessionOptions = { extensionPath: "/test/extension", provider: "openrouter", apiKey: "test-key", diff --git a/apps/cli/src/acp/__tests__/model-service.test.ts b/apps/cli/src/acp/__tests__/model-service.test.ts index 7931b8b5b1..ad4f83e28a 100644 --- a/apps/cli/src/acp/__tests__/model-service.test.ts +++ b/apps/cli/src/acp/__tests__/model-service.test.ts @@ -118,26 +118,65 @@ describe("ModelService", () => { expect(result).toEqual(DEFAULT_MODELS) }) - it("should transform API response to AcpModel format", async () => { + it("should transform API response to AcpModel format using name and description fields", async () => { const service = new ModelService() mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ data: [ - { id: "anthropic/claude-3-sonnet", owned_by: "anthropic" }, - { id: "openai/gpt-4", owned_by: "openai" }, + { + id: "anthropic/claude-3-sonnet", + name: "Claude 3 Sonnet", + description: "A balanced model for most tasks", + owned_by: "anthropic", + }, + { + id: "openai/gpt-4", + name: "GPT-4", + description: "OpenAI's flagship model", + owned_by: "openai", + }, ], }), }) const result = await service.fetchAvailableModels() - // Should include default model first with actual model name - expect(result[0]).toEqual(DEFAULT_MODELS[0]) + // Should include transformed models with name and description from API + expect(result).toHaveLength(2) + expect(result).toContainEqual({ + modelId: "anthropic/claude-3-sonnet", + name: "Claude 3 Sonnet", + description: "A balanced model for most tasks", + }) + expect(result).toContainEqual({ + modelId: "openai/gpt-4", + name: "GPT-4", + description: "OpenAI's flagship model", + }) + }) - // Should include transformed models - expect(result.length).toBeGreaterThan(1) + it("should sort models by model ID", async () => { + const service = new ModelService() + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [ + { id: "openai/gpt-4", name: "GPT-4" }, + { id: "anthropic/claude-3-sonnet", name: "Claude 3 Sonnet" }, + { id: "google/gemini-pro", name: "Gemini Pro" }, + ], + }), + }) + + const result = await service.fetchAvailableModels() + + // Should be sorted by model ID + expect(result[0]!.modelId).toBe("anthropic/claude-3-sonnet") + expect(result[1]!.modelId).toBe("google/gemini-pro") + expect(result[2]!.modelId).toBe("openai/gpt-4") }) it("should include Authorization header when apiKey is provided", async () => { @@ -161,41 +200,6 @@ describe("ModelService", () => { }) }) - describe("getModelState", () => { - it("should return model state with current model ID", async () => { - const service = new ModelService() - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - data: [{ id: "anthropic/claude-sonnet-4.5" }], - }), - }) - - const state = await service.getModelState("anthropic/claude-sonnet-4.5") - - expect(state).toEqual({ - availableModels: expect.any(Array), - currentModelId: "anthropic/claude-sonnet-4.5", - }) - }) - - it("should fall back to 'default' if current model ID is not in available models", async () => { - const service = new ModelService() - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ - data: [{ id: "model-1" }], - }), - }) - - const state = await service.getModelState("non-existent-model") - - expect(state.currentModelId).toBe(DEFAULT_MODELS[0]!.modelId) - }) - }) - describe("clearCache", () => { it("should clear the cached models", async () => { const service = new ModelService() diff --git a/apps/cli/src/acp/__tests__/session.test.ts b/apps/cli/src/acp/__tests__/session.test.ts index 21586853a0..e5655c00cd 100644 --- a/apps/cli/src/acp/__tests__/session.test.ts +++ b/apps/cli/src/acp/__tests__/session.test.ts @@ -93,20 +93,26 @@ describe("AcpSession", () => { describe("create", () => { it("should create a session with a unique ID", async () => { - const session = await AcpSession.create( - "test-session-1", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session-1", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) expect(session).toBeDefined() expect(session.getSessionId()).toBe("test-session-1") }) it("should create ExtensionHost with correct config", async () => { - await AcpSession.create("test-session-2", "/test/workspace", mockConnection, undefined, defaultOptions) + await AcpSession.create({ + sessionId: "test-session-2", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) expect(ExtensionHost).toHaveBeenCalledWith( expect.objectContaining({ @@ -121,26 +127,25 @@ describe("AcpSession", () => { }) it("should accept client capabilities", async () => { - const clientCapabilities: acp.ClientCapabilities = { - fs: { - readTextFile: true, - writeTextFile: true, - }, - } - - const session = await AcpSession.create( - "test-session-3", - "/test/workspace", - mockConnection, - clientCapabilities, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session-3", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) expect(session).toBeDefined() }) it("should activate the extension host", async () => { - await AcpSession.create("test-session-4", "/test/workspace", mockConnection, undefined, defaultOptions) + await AcpSession.create({ + sessionId: "test-session-4", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value expect(mockHostInstance.activate).toHaveBeenCalled() @@ -149,13 +154,13 @@ describe("AcpSession", () => { describe("prompt", () => { it("should send a task to the extension host", async () => { - const session = await AcpSession.create( - "test-session", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value @@ -181,13 +186,13 @@ describe("AcpSession", () => { }) it("should handle image prompts", async () => { - const session = await AcpSession.create( - "test-session", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value @@ -215,13 +220,13 @@ describe("AcpSession", () => { describe("cancel", () => { it("should send cancel message to extension host", async () => { - const session = await AcpSession.create( - "test-session", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value @@ -243,13 +248,13 @@ describe("AcpSession", () => { describe("setMode", () => { it("should update the session mode", async () => { - const session = await AcpSession.create( - "test-session", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value @@ -264,13 +269,13 @@ describe("AcpSession", () => { describe("dispose", () => { it("should dispose the extension host", async () => { - const session = await AcpSession.create( - "test-session", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "test-session", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) const mockHostInstance = vi.mocked(ExtensionHost).mock.results[0]!.value @@ -282,13 +287,13 @@ describe("AcpSession", () => { describe("getSessionId", () => { it("should return the session ID", async () => { - const session = await AcpSession.create( - "my-unique-session-id", - "/test/workspace", - mockConnection, - undefined, - defaultOptions, - ) + const session = await AcpSession.create({ + sessionId: "my-unique-session-id", + cwd: "/test/workspace", + connection: mockConnection, + options: defaultOptions, + deps: {}, + }) expect(session.getSessionId()).toBe("my-unique-session-id") }) diff --git a/apps/cli/src/acp/agent.ts b/apps/cli/src/acp/agent.ts index 7557419911..038a74cba4 100644 --- a/apps/cli/src/acp/agent.ts +++ b/apps/cli/src/acp/agent.ts @@ -5,65 +5,39 @@ * This allows ACP clients like Zed to use Roo Code as their AI coding assistant. */ -import * as acp from "@agentclientprotocol/sdk" +import { + type Agent, + type ClientCapabilities, + type CancelNotification, + // Requests + Responses + type InitializeRequest, + type InitializeResponse, + type NewSessionRequest, + type NewSessionResponse, + type SetSessionModeRequest, + type SetSessionModeResponse, + type SetSessionModelRequest, + type SetSessionModelResponse, + type AuthenticateRequest, + type AuthenticateResponse, + type PromptRequest, + type PromptResponse, + // Classes + AgentSideConnection, + RequestError, + // Constants + PROTOCOL_VERSION, +} from "@agentclientprotocol/sdk" import { randomUUID } from "node:crypto" -import { login, status } from "@/commands/auth/index.js" import { DEFAULT_FLAGS } from "@/types/constants.js" +import { envVarMap } from "@/lib/utils/provider.js" +import { login, status } from "@/commands/auth/index.js" -import { AcpSession, type AcpSessionOptions } from "./session.js" +import { AVAILABLE_MODES, DEFAULT_MODELS } from "./types.js" +import { type AcpSessionOptions, AcpSession } from "./session.js" import { acpLog } from "./logger.js" import { ModelService, createModelService } from "./model-service.js" -import { type ExtendedNewSessionResponse, type AcpModelState } from "./types.js" -import { envVarMap } from "@/lib/utils/provider.js" - -// ============================================================================= -// Types -// ============================================================================= - -export interface RooCodeAgentOptions { - /** Path to the extension bundle */ - extensionPath: string - /** API provider (defaults to openrouter) */ - provider?: string - /** API key (optional, may come from environment) */ - apiKey?: string - /** Model to use (defaults to a sensible default) */ - model?: string - /** Initial mode (defaults to code) */ - mode?: string -} - -// ============================================================================= -// Available Modes -// ============================================================================= - -const AVAILABLE_MODES: acp.SessionMode[] = [ - { - id: "code", - name: "Code", - description: "Write, modify, and refactor code", - }, - { - id: "architect", - name: "Architect", - description: "Plan and design system architecture", - }, - { - id: "ask", - name: "Ask", - description: "Ask questions and get explanations", - }, - { - id: "debug", - name: "Debug", - description: "Debug issues and troubleshoot problems", - }, -] - -// ============================================================================= -// RooCodeAgent Class -// ============================================================================= /** * RooCodeAgent implements the ACP Agent interface. @@ -71,42 +45,30 @@ const AVAILABLE_MODES: acp.SessionMode[] = [ * It manages multiple sessions, each with its own ExtensionHost instance, * and handles protocol-level operations like initialization and authentication. */ -export class RooCodeAgent implements acp.Agent { +export class RooCodeAgent implements Agent { private sessions: Map = new Map() - private clientCapabilities: acp.ClientCapabilities | undefined + private clientCapabilities: ClientCapabilities | undefined private isAuthenticated = false private readonly modelService: ModelService constructor( - private readonly options: RooCodeAgentOptions, - private readonly connection: acp.AgentSideConnection, + private readonly options: AcpSessionOptions, + private readonly connection: AgentSideConnection, ) { - // Initialize model service with optional API key - this.modelService = createModelService({ - apiKey: options.apiKey, - }) + acpLog.info("Agent", `RooCodeAgent constructor: connection=${connection}`) + this.modelService = createModelService({ apiKey: options.apiKey }) } - // =========================================================================== - // Initialization - // =========================================================================== - - /** - * Initialize the agent and exchange capabilities with the client. - */ - async initialize(params: acp.InitializeRequest): Promise { - acpLog.request("initialize", { protocolVersion: params.protocolVersion }) - + async initialize(params: InitializeRequest): Promise { + acpLog.request("initialize", params) this.clientCapabilities = params.clientCapabilities - acpLog.debug("Agent", "Client capabilities", this.clientCapabilities) - // Check if already authenticated via environment or existing credentials - const authStatus = await status({ verbose: false }) - this.isAuthenticated = authStatus.authenticated - acpLog.debug("Agent", `Auth status: ${this.isAuthenticated ? "authenticated" : "not authenticated"}`) + // Check if already authenticated via environment or existing credentials. + const { authenticated } = await status({ verbose: false }) + acpLog.debug("Agent", `Auth status: ${authenticated ? "authenticated" : "not authenticated"}`) - const response: acp.InitializeResponse = { - protocolVersion: acp.PROTOCOL_VERSION, + return { + protocolVersion: PROTOCOL_VERSION, authMethods: [ { id: "roo", @@ -122,27 +84,118 @@ export class RooCodeAgent implements acp.Agent { }, }, } + } - acpLog.response("initialize", response) + async newSession(params: NewSessionRequest): Promise { + acpLog.request("newSession", params) + + // @TODO: Detect other env vars for different provider and choose + // the correct provider or throw. + if (!this.isAuthenticated) { + const apiKey = this.options.apiKey || process.env.OPENROUTER_API_KEY + + if (!apiKey) { + acpLog.error("Agent", "newSession failed: not authenticated and no API key") + throw RequestError.authRequired() + } + + this.isAuthenticated = true + } + + const sessionId = randomUUID() + const provider = this.options.provider || "openrouter" + const apiKey = this.options.apiKey || process.env.OPENROUTER_API_KEY + const mode = this.options.mode || AVAILABLE_MODES[0]!.id + const model = this.options.model || DEFAULT_FLAGS.model + + const session = await AcpSession.create({ + sessionId, + cwd: params.cwd, + connection: this.connection, + options: { + extensionPath: this.options.extensionPath, + provider, + apiKey, + model, + mode, + }, + deps: { + logger: acpLog, + }, + }) + + this.sessions.set(sessionId, session) + + const availableModels = await this.modelService.fetchAvailableModels() + const modelExists = availableModels.some((m) => m.modelId === model) + + const response: NewSessionResponse = { + sessionId, + modes: { currentModeId: mode, availableModes: AVAILABLE_MODES }, + models: { + availableModels, + currentModelId: modelExists ? model : DEFAULT_MODELS[0]!.modelId, + }, + } + + acpLog.response("newSession", response) return response } - // =========================================================================== - // Authentication - // =========================================================================== + async setSessionMode(params: SetSessionModeRequest): Promise { + acpLog.request("setSessionMode", params) + const session = this.sessions.get(params.sessionId) + + if (!session) { + acpLog.error("Agent", `setSessionMode failed: session not found: ${params.sessionId}`) + throw RequestError.invalidParams(undefined, `Session not found: ${params.sessionId}`) + } + + const mode = AVAILABLE_MODES.find((m) => m.id === params.modeId) + + if (!mode) { + acpLog.error("Agent", `setSessionMode failed: unknown mode: ${params.modeId}`) + throw RequestError.invalidParams(undefined, `Unknown mode: ${params.modeId}`) + } + + session.setMode(params.modeId) + acpLog.response("setSessionMode", {}) + return {} + } + + async unstable_setSessionModel?(params: SetSessionModelRequest): Promise { + acpLog.request("setSessionMode", params) + const session = this.sessions.get(params.sessionId) + + if (!session) { + acpLog.error("Agent", `unstable_setSessionModel failed: session not found: ${params.sessionId}`) + throw RequestError.invalidParams(undefined, `Session not found: ${params.sessionId}`) + } + + const availableModels = await this.modelService.fetchAvailableModels() + const modelExists = availableModels.some((m) => m.modelId === params.modelId) + + if (!modelExists) { + acpLog.error("Agent", `unstable_setSessionModel failed: model not found: ${params.modelId}`) + throw RequestError.invalidParams(undefined, `Model not found: ${params.modelId}`) + } + + session.setModel(params.modelId) + acpLog.response("unstable_setSessionModel", {}) + return {} + } + + async authenticate(params: AuthenticateRequest): Promise { + acpLog.request("authenticate", params) - /** - * Authenticate with Roo Code Cloud. - */ - async authenticate(params: acp.AuthenticateRequest): Promise { if (params.methodId !== "roo") { - throw acp.RequestError.invalidParams(undefined, `Invalid auth method: ${params.methodId}`) + throw RequestError.invalidParams(undefined, `Invalid auth method: ${params.methodId}`) } const result = await login({ verbose: false }) if (!result.success) { - throw acp.RequestError.authRequired(undefined, "Failed to authenticate with Roo Code Cloud") + throw RequestError.authRequired(undefined, "Failed to authenticate with Roo Code Cloud") } this.isAuthenticated = true @@ -151,90 +204,7 @@ export class RooCodeAgent implements acp.Agent { return {} } - // =========================================================================== - // Session Management - // =========================================================================== - - /** - * Create a new session. - */ - async newSession(params: acp.NewSessionRequest): Promise { - acpLog.request("newSession", { cwd: params.cwd }) - - // Require authentication - if (!this.isAuthenticated) { - // Check if API key is available - const apiKey = this.options.apiKey || process.env.OPENROUTER_API_KEY - if (!apiKey) { - acpLog.error("Agent", "newSession failed: not authenticated and no API key") - throw acp.RequestError.authRequired() - } - this.isAuthenticated = true - } - - const sessionId = randomUUID() - const initialMode = this.options.mode || "code" - acpLog.info("Agent", `Creating new session: ${sessionId}`) - - const sessionOptions: AcpSessionOptions = { - extensionPath: this.options.extensionPath, - provider: this.options.provider || "openrouter", - apiKey: this.options.apiKey || process.env.OPENROUTER_API_KEY, - model: this.options.model || DEFAULT_FLAGS.model, - mode: initialMode, - } - - acpLog.debug("Agent", "Session options", { - extensionPath: sessionOptions.extensionPath, - provider: sessionOptions.provider, - model: sessionOptions.model, - mode: sessionOptions.mode, - }) - - const session = await AcpSession.create( - sessionId, - params.cwd, - this.connection, - this.clientCapabilities, - sessionOptions, - ) - - this.sessions.set(sessionId, session) - acpLog.info("Agent", `Session created successfully: ${sessionId}`) - - // Fetch model state asynchronously (don't block session creation) - const modelState = await this.getModelState() - - // Build response with modes and models - const response: ExtendedNewSessionResponse = { - sessionId, - modes: { - currentModeId: initialMode, - availableModes: AVAILABLE_MODES, - }, - models: modelState, - } - - acpLog.response("newSession", response) - return response - } - - /** - * Get the current model state. - */ - private async getModelState(): Promise { - const currentModelId = this.options.model || DEFAULT_FLAGS.model - return this.modelService.getModelState(currentModelId) - } - - // =========================================================================== - // Prompt Handling - // =========================================================================== - - /** - * Process a prompt request. - */ - async prompt(params: acp.PromptRequest): Promise { + async prompt(params: PromptRequest): Promise { acpLog.request("prompt", { sessionId: params.sessionId, promptLength: params.prompt?.length ?? 0, @@ -243,7 +213,7 @@ export class RooCodeAgent implements acp.Agent { const session = this.sessions.get(params.sessionId) if (!session) { acpLog.error("Agent", `prompt failed: session not found: ${params.sessionId}`) - throw acp.RequestError.invalidParams(undefined, `Session not found: ${params.sessionId}`) + throw RequestError.invalidParams(undefined, `Session not found: ${params.sessionId}`) } const response = await session.prompt(params) @@ -251,14 +221,7 @@ export class RooCodeAgent implements acp.Agent { return response } - // =========================================================================== - // Session Control - // =========================================================================== - - /** - * Cancel an ongoing prompt. - */ - async cancel(params: acp.CancelNotification): Promise { + async cancel(params: CancelNotification): Promise { acpLog.request("cancel", { sessionId: params.sessionId }) const session = this.sessions.get(params.sessionId) @@ -270,37 +233,6 @@ export class RooCodeAgent implements acp.Agent { } } - /** - * Set the session mode. - */ - async setSessionMode(params: acp.SetSessionModeRequest): Promise { - acpLog.request("setSessionMode", { sessionId: params.sessionId, modeId: params.modeId }) - - const session = this.sessions.get(params.sessionId) - if (!session) { - acpLog.error("Agent", `setSessionMode failed: session not found: ${params.sessionId}`) - throw acp.RequestError.invalidParams(undefined, `Session not found: ${params.sessionId}`) - } - - const mode = AVAILABLE_MODES.find((m) => m.id === params.modeId) - if (!mode) { - acpLog.error("Agent", `setSessionMode failed: unknown mode: ${params.modeId}`) - throw acp.RequestError.invalidParams(undefined, `Unknown mode: ${params.modeId}`) - } - - session.setMode(params.modeId) - acpLog.info("Agent", `Set session ${params.sessionId} mode to: ${params.modeId}`) - acpLog.response("setSessionMode", {}) - return {} - } - - // =========================================================================== - // Cleanup - // =========================================================================== - - /** - * Dispose of all sessions and cleanup. - */ async dispose(): Promise { acpLog.info("Agent", `Disposing ${this.sessions.size} sessions`) const disposals = Array.from(this.sessions.values()).map((session) => session.dispose()) diff --git a/apps/cli/src/acp/command-stream.ts b/apps/cli/src/acp/command-stream.ts index 4bc09f1235..daff9c2c61 100644 --- a/apps/cli/src/acp/command-stream.ts +++ b/apps/cli/src/acp/command-stream.ts @@ -3,8 +3,6 @@ * * Manages streaming of command execution output with code fence wrapping. * Handles both live command execution events and final command_output messages. - * - * Extracted from session.ts to separate the command output streaming concern. */ import type { ClineMessage } from "@roo-code/types" @@ -106,33 +104,36 @@ export class CommandStreamManager { const output = message.text || "" const isPartial = message.partial === true - // Skip partial updates - streaming is handled by handleExecutionOutput() + // Skip partial updates - streaming is handled by handleExecutionOutput(). if (isPartial) { return } - // Handle completion - update the tool call UI + // Handle completion - update the tool call UI. const pendingCall = this.findMostRecentPendingCommand() if (pendingCall) { - // Send closing code fence as agent_message_chunk if we had streaming output + // Send closing code fence as agent_message_chunk if we had streaming output. const hadStreamingOutput = this.commandCodeFencesSent.has(pendingCall.toolCallId) + if (hadStreamingOutput) { this.sendUpdate({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "```\n" }, }) + this.commandCodeFencesSent.delete(pendingCall.toolCallId) } - // Command completed - send final tool_call_update with completed status - // Note: Zed doesn't display tool_call_update content, so we just mark it complete + // Command completed - send final tool_call_update with completed status. + // Note: Zed doesn't display tool_call_update content, so we just mark it complete. this.sendUpdate({ sessionUpdate: "tool_call_update", toolCallId: pendingCall.toolCallId, status: "completed", rawOutput: { output }, }) + this.pendingCommandCalls.delete(pendingCall.toolCallId) } } @@ -152,21 +153,24 @@ export class CommandStreamManager { * Uses executionId → toolCallId mapping for robust routing. */ handleExecutionOutput(executionId: string, output: string): void { - // Find or establish the toolCallId for this executionId + // Find or establish the toolCallId for this executionId. let toolCallId = this.executionToToolCallId.get(executionId) if (!toolCallId) { - // First output for this executionId - establish the mapping + // First output for this executionId - establish the mapping. const pendingCall = this.findMostRecentPendingCommand() + if (!pendingCall) { return } + toolCallId = pendingCall.toolCallId this.executionToToolCallId.set(executionId, toolCallId) } - // Use executionId as the message key for delta tracking + // Use executionId as the message key for delta tracking. const delta = this.deltaTracker.getDelta(executionId, output) + if (!delta) { return } @@ -175,13 +179,14 @@ export class CommandStreamManager { const isFirstChunk = !this.commandCodeFencesSent.has(toolCallId) if (isFirstChunk) { this.commandCodeFencesSent.add(toolCallId) + this.sendUpdate({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "```\n" }, }) } - // Send the delta as agent_message_chunk for Zed visibility + // Send the delta as agent_message_chunk for Zed visibility. this.sendUpdate({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: delta }, @@ -201,7 +206,7 @@ export class CommandStreamManager { */ reset(): void { // Clear all pending commands - any from previous prompts are now stale - // and would cause duplicate completion messages if not cleaned up + // and would cause duplicate completion messages if not cleaned up. this.pendingCommandCalls.clear() this.commandCodeFencesSent.clear() this.executionToToolCallId.clear() @@ -221,10 +226,6 @@ export class CommandStreamManager { return this.commandCodeFencesSent.size > 0 } - // =========================================================================== - // Private Methods - // =========================================================================== - /** * Find the most recent pending command call. */ diff --git a/apps/cli/src/acp/index.ts b/apps/cli/src/acp/index.ts index 0105586690..319b1294c9 100644 --- a/apps/cli/src/acp/index.ts +++ b/apps/cli/src/acp/index.ts @@ -1,186 +1,2 @@ -// Main agent exports -export { type RooCodeAgentOptions, RooCodeAgent } from "./agent.js" -export { type AcpSessionOptions, AcpSession } from "./session.js" - -// Types for mode and model pickers -export type { AcpModel, AcpModelState, ExtendedNewSessionResponse } from "./types.js" -export { DEFAULT_MODELS } from "./types.js" - -// Model service -export { ModelService, createModelService, type ModelServiceOptions } from "./model-service.js" - -// Interfaces for dependency injection -export type { - IAcpLogger, - IAcpSession, - IContentFormatter, - IExtensionClient, - IExtensionHost, - IDeltaTracker, - IPromptStateMachine, - ICommandStreamManager, - IToolContentStreamManager, - AcpSessionDependencies, - SendUpdateFn, - PromptStateType, - PromptCompletionResult, - StreamManagerOptions, -} from "./interfaces.js" -export { NullLogger } from "./interfaces.js" - -// Logger +export { RooCodeAgent } from "./agent.js" export { acpLog } from "./logger.js" - -// Utilities -export { DeltaTracker } from "./delta-tracker.js" - -// Shared utility functions -export { - // Result type - type Result, - ok, - err, - // Formatting functions - formatSearchResults, - formatReadContent, - wrapInCodeBlock, - // Content extraction - extractContentFromParams, - // File operations - readFileContent, - readFileContentAsync, - resolveFilePath, - resolveFilePathUnsafe, - // Validation - isUserEcho, - hasValidFilePath, - // Config - type FormatConfig, - DEFAULT_FORMAT_CONFIG, -} from "./utils/index.js" - -// Tool Registry -export { - // Categories - TOOL_CATEGORIES, - type ToolCategory, - type KnownToolName, - // Detection functions - isEditTool, - isReadTool, - isSearchTool, - isListFilesTool, - isExecuteTool, - isDeleteTool, - isMoveTool, - isThinkTool, - isFetchTool, - isSwitchModeTool, - isFileWriteTool, - // Kind mapping - mapToolToKind, - // Validation schemas - FilePathParamsSchema, - FileWriteParamsSchema, - FileMoveParamsSchema, - SearchParamsSchema, - ListFilesParamsSchema, - CommandParamsSchema, - ThinkParamsSchema, - SwitchModeParamsSchema, - GenericToolParamsSchema, - ToolMessageSchema, - // Parameter types - type FilePathParams, - type FileWriteParams, - type FileMoveParams, - type SearchParams, - type ListFilesParams, - type CommandParams, - type ThinkParams, - type SwitchModeParams, - type GenericToolParams, - type ToolParams, - type ToolMessage, - // Validation functions - type ValidationResult, - validateToolParams, - parseToolParams, - parseToolMessage, -} from "./tool-registry.js" - -// State management -export { PromptStateMachine, createPromptStateMachine, type PromptStateMachineOptions } from "./prompt-state.js" - -// Content formatting -export { - // Direct function exports (preferred for simple use) - formatToolResult, - extractFileContent, - extractFileContentAsync, - // Re-exported utilities - formatSearchResults as formatSearch, - formatReadContent as formatRead, - wrapInCodeBlock as wrapCode, - isUserEcho as checkUserEcho, - // Class-based DI - ContentFormatter, - createContentFormatter, - type ContentFormatterConfig, -} from "./content-formatter.js" - -// Tool handlers -export { - type ToolHandler, - type ToolHandlerContext, - type ToolHandleResult, - ToolHandlerRegistry, - // Individual handlers for extension - CommandToolHandler, - FileEditToolHandler, - FileReadToolHandler, - SearchToolHandler, - ListFilesToolHandler, - DefaultToolHandler, -} from "./tool-handler.js" - -// Stream managers -export { CommandStreamManager, type PendingCommand, type CommandStreamManagerOptions } from "./command-stream.js" -export { ToolContentStreamManager, type ToolContentStreamManagerOptions } from "./tool-content-stream.js" - -// Session event handler -export { - SessionEventHandler, - createSessionEventHandler, - type SessionEventHandlerDeps, - type TaskCompletedCallback, -} from "./session-event-handler.js" - -// Translation utilities -export { - // Message translation - translateToAcpUpdate, - isPermissionAsk, - isCompletionAsk, - createPermissionOptions, - // Tool parsing - parseToolFromMessage, - generateToolTitle, - extractToolContent, - buildToolCallFromMessage, - type ToolCallInfo, - // Prompt extraction - extractPromptText, - extractPromptImages, - extractPromptResources, - // Location extraction - extractLocations, - extractFilePathsFromSearchResults, - type LocationParams, - // Diff parsing - parseUnifiedDiff, - isUnifiedDiff, - type ParsedDiff, - // Backward compatibility - mapToolKind, -} from "./translator.js" diff --git a/apps/cli/src/acp/model-service.ts b/apps/cli/src/acp/model-service.ts index 17da48d84d..e4c8bfc01b 100644 --- a/apps/cli/src/acp/model-service.ts +++ b/apps/cli/src/acp/model-service.ts @@ -4,49 +4,32 @@ * Fetches and caches available models from the Roo Code API. */ -import type { AcpModel, AcpModelState } from "./types.js" +import type { ModelInfo } from "@agentclientprotocol/sdk" + import { DEFAULT_MODELS } from "./types.js" import { acpLog } from "./logger.js" -// ============================================================================= -// Types -// ============================================================================= +const DEFAULT_API_URL = "https://api.roocode.com" +const DEFAULT_TIMEOUT = 5_000 + +interface RooModel { + id: string + name: string + description?: string + object?: string + created?: number + owned_by?: string +} export interface ModelServiceOptions { - /** Base URL for the API (defaults to https://api.roocode.com) */ + /** Base URL for the API (defaults to DEFAULT_API_URL) */ apiUrl?: string /** API key for authentication */ apiKey?: string - /** Request timeout in milliseconds (defaults to 5000) */ + /** Request timeout in milliseconds (defaults to DEFAULT_TIMEOUT) */ timeout?: number } -/** - * Response structure from /proxy/v1/models endpoint. - * Based on OpenAI-compatible model listing format. - */ -interface ModelsApiResponse { - object?: string - data?: Array<{ - id: string - object?: string - created?: number - owned_by?: string - // Additional fields may be present - }> -} - -// ============================================================================= -// Constants -// ============================================================================= - -const DEFAULT_API_URL = "https://api.roocode.com" -const DEFAULT_TIMEOUT = 5000 - -// ============================================================================= -// ModelService Class -// ============================================================================= - /** * Service for fetching and managing available models. */ @@ -54,7 +37,7 @@ export class ModelService { private readonly apiUrl: string private readonly apiKey?: string private readonly timeout: number - private cachedModels: AcpModel[] | null = null + private cachedModels: ModelInfo[] | null = null constructor(options: ModelServiceOptions = {}) { this.apiUrl = options.apiUrl || DEFAULT_API_URL @@ -67,8 +50,7 @@ export class ModelService { * Returns cached models if available, otherwise fetches from API. * Falls back to default models on error. */ - async fetchAvailableModels(): Promise { - // Return cached models if available + async fetchAvailableModels(): Promise { if (this.cachedModels) { return this.cachedModels } @@ -99,7 +81,7 @@ export class ModelService { return this.cachedModels } - const data = (await response.json()) as ModelsApiResponse + const data = await response.json() if (!data.data || !Array.isArray(data.data)) { acpLog.warn("ModelService", "Invalid API response format, using default models") @@ -107,10 +89,7 @@ export class ModelService { return this.cachedModels } - // Transform API response to AcpModel format - this.cachedModels = this.transformApiResponse(data.data) - acpLog.debug("ModelService", `Fetched ${this.cachedModels.length} models from API`) - + this.cachedModels = this.translateModels(data.data) return this.cachedModels } catch (error) { if (error instanceof Error && error.name === "AbortError") { @@ -121,27 +100,12 @@ export class ModelService { `Failed to fetch models: ${error instanceof Error ? error.message : String(error)}`, ) } + this.cachedModels = DEFAULT_MODELS return this.cachedModels } } - /** - * Get the current model state including available models and current selection. - */ - async getModelState(currentModelId: string): Promise { - const availableModels = await this.fetchAvailableModels() - - // Validate that currentModelId exists in available models - const modelExists = availableModels.some((m) => m.modelId === currentModelId) - const effectiveModelId = modelExists ? currentModelId : DEFAULT_MODELS[0]!.modelId - - return { - availableModels, - currentModelId: effectiveModelId, - } - } - /** * Clear the cached models, forcing a refresh on next fetch. */ @@ -149,68 +113,15 @@ export class ModelService { this.cachedModels = null } - /** - * Transform API response to AcpModel format. - */ - private transformApiResponse( - data: Array<{ - id: string - object?: string - created?: number - owned_by?: string - }>, - ): AcpModel[] { - // If API returns models, transform them - // For now, we'll create a simple mapping - // In practice, the API should return model metadata including pricing - const models: AcpModel[] = [] + private translateModels(data: RooModel[]): ModelInfo[] { + const models: ModelInfo[] = data + .map(({ id, name, description }) => ({ modelId: id, name, description })) + .sort((a, b) => a.modelId.localeCompare(b.modelId)) - const defaultModel = DEFAULT_MODELS[0]! - - // Always include the default model first (shows actual model name) - models.push(defaultModel) - - // Add models from API response - for (const model of data) { - // Skip if it's already in our list or if it's a system model - if (model.id === defaultModel.modelId || model.id.startsWith("_")) { - continue - } - - models.push({ - modelId: model.id, - name: this.formatModelName(model.id), - description: model.owned_by ? `Provided by ${model.owned_by}` : undefined, - }) - } - - // If no models from API, return defaults - if (models.length === 1) { - return DEFAULT_MODELS - } - - return models - } - - /** - * Format a model ID into a human-readable name. - */ - private formatModelName(modelId: string): string { - // Convert model IDs like "anthropic/claude-3-sonnet" to "Claude 3 Sonnet" - const parts = modelId.split("/") - const name = parts[parts.length - 1] || modelId - - return name - .split("-") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" ") + return models.length === 0 ? DEFAULT_MODELS : models } } -// ============================================================================= -// Factory Function -// ============================================================================= - /** * Create a new ModelService instance. */ diff --git a/apps/cli/src/acp/session-event-handler.ts b/apps/cli/src/acp/session-event-handler.ts index 07f0f9e1e0..c10afee290 100644 --- a/apps/cli/src/acp/session-event-handler.ts +++ b/apps/cli/src/acp/session-event-handler.ts @@ -2,11 +2,18 @@ * Session Event Handler * * Handles events from the ExtensionClient and ExtensionHost, translating them to ACP updates. - * Extracted from session.ts for better separation of concerns. */ import type { SessionMode } from "@agentclientprotocol/sdk" -import type { ClineMessage, ClineAsk, ClineSay, ExtensionMessage, ExtensionState, ModeConfig } from "@roo-code/types" +import type { + ClineMessage, + ClineAsk, + ClineSay, + ExtensionMessage, + ExtensionState, + WebviewMessage, + ModeConfig, +} from "@roo-code/types" import type { WaitingForInputEvent, TaskCompletedEvent, CommandExecutionOutputEvent } from "@/agent/events.js" @@ -123,7 +130,7 @@ export interface SessionEventHandlerDeps { /** Callback to respond with text */ respondWithText: (text: string) => void /** Callback to send message to extension */ - sendToExtension: (message: unknown) => void + sendToExtension: (message: WebviewMessage) => void /** Workspace path */ workspacePath: string /** Initial mode ID */ @@ -169,7 +176,7 @@ export class SessionEventHandler { private readonly sendUpdate: SendUpdateFn private readonly approveAction: () => void private readonly respondWithText: (text: string) => void - private readonly sendToExtension: (message: unknown) => void + private readonly sendToExtension: (message: WebviewMessage) => void private readonly workspacePath: string private readonly isCancelling: () => boolean diff --git a/apps/cli/src/acp/session.ts b/apps/cli/src/acp/session.ts index b5843dd80b..efb5145e90 100644 --- a/apps/cli/src/acp/session.ts +++ b/apps/cli/src/acp/session.ts @@ -6,14 +6,15 @@ */ import { - type SessionNotification, - type ClientCapabilities, + type SessionUpdate, type PromptRequest, type PromptResponse, type SessionModeState, AgentSideConnection, } from "@agentclientprotocol/sdk" +import type { SupportedProvider } from "@/types/types.js" +import { getProviderSettings } from "@/lib/utils/provider.js" import { type ExtensionHostOptions, ExtensionHost } from "@/agent/extension-host.js" import { AgentLoopState } from "@/agent/agent-state.js" @@ -35,20 +36,11 @@ import type { } from "./interfaces.js" import { type Result, ok, err } from "./utils/index.js" -// ============================================================================= -// Types -// ============================================================================= - export interface AcpSessionOptions { - /** Path to the extension bundle */ extensionPath: string - /** API provider */ - provider: string - /** API key (optional, may come from environment) */ + provider: SupportedProvider apiKey?: string - /** Model to use */ model: string - /** Initial mode */ mode: string } @@ -81,9 +73,6 @@ export class AcpSession implements IAcpSession { /** Session event handler for managing extension events */ private readonly eventHandler: SessionEventHandler - /** Workspace path for resolving relative file paths */ - private readonly workspacePath: string - /** Current model ID */ private currentModelId: string = DEFAULT_MODELS[0]!.modelId @@ -94,27 +83,18 @@ export class AcpSession implements IAcpSession { private readonly sessionId: string, private readonly extensionHost: ExtensionHost, private readonly connection: AgentSideConnection, - workspacePath: string, - initialMode: string, + private readonly workspacePath: string, + private readonly options: AcpSessionOptions, deps: AcpSessionDependencies = {}, ) { - this.workspacePath = workspacePath - - // Initialize dependencies with defaults or injected instances. this.logger = deps.logger ?? acpLog this.promptState = deps.createPromptStateMachine?.() ?? new PromptStateMachine({ logger: this.logger }) this.deltaTracker = deps.createDeltaTracker?.() ?? new DeltaTracker() - // Initialize tool handler registry. + const sendUpdate = (update: SessionUpdate) => connection.sessionUpdate({ sessionId: this.sessionId, update }) + this.toolHandlerRegistry = new ToolHandlerRegistry() - // Create send update callback for stream managers. - // Updates are sent directly to preserve chunk ordering. - const sendUpdate = (update: SessionNotification["update"]) => { - void this.sendUpdateDirect(update) - } - - // Initialize stream managers with injected logger. this.commandStreamManager = new CommandStreamManager({ deltaTracker: this.deltaTracker, sendUpdate, @@ -127,7 +107,7 @@ export class AcpSession implements IAcpSession { logger: this.logger, }) - // Create event handler with extension host for mode tracking + // Create event handler with extension host for mode tracking. this.eventHandler = createSessionEventHandler({ logger: this.logger, client: extensionHost.client, @@ -139,22 +119,21 @@ export class AcpSession implements IAcpSession { toolHandlerRegistry: this.toolHandlerRegistry, sendUpdate, approveAction: () => this.extensionHost.client.approve(), - respondWithText: (text: string) => this.extensionHost.client.respond(text), - sendToExtension: (message) => - this.extensionHost.sendToExtension(message as Parameters[0]), + respondWithText: (text: string, images?: string[]) => this.extensionHost.client.respond(text, images), + sendToExtension: (message) => this.extensionHost.sendToExtension(message), workspacePath, - initialModeId: initialMode, + initialModeId: this.options.mode, isCancelling: () => this.isCancelling, }) this.eventHandler.onTaskCompleted((success) => this.handleTaskCompleted(success)) - // Listen for state changes to log and detect cancellation completion + // Listen for state changes to log and detect cancellation completion. this.extensionHost.client.on("stateChange", (event) => { const prev = event.previousState const curr = event.currentState - // Only log if something actually changed + // Only log if something actually changed. const stateChanged = prev.state !== curr.state || prev.isRunning !== curr.isRunning || @@ -188,32 +167,25 @@ export class AcpSession implements IAcpSession { }) } - // =========================================================================== - // Factory Method - // =========================================================================== - /** * Create a new AcpSession. * * This initializes an ExtensionHost for the given working directory * and sets up event handlers to stream updates to the ACP client. - * - * @param sessionId - Unique session identifier - * @param cwd - Working directory for the session - * @param connection - ACP connection for sending updates - * @param _clientCapabilities - Client capabilities (currently unused) - * @param options - Session configuration options - * @param deps - Optional dependencies for testing */ - static async create( - sessionId: string, - cwd: string, - connection: AgentSideConnection, - _clientCapabilities: ClientCapabilities | undefined, - options: AcpSessionOptions, - deps: AcpSessionDependencies = {}, - ): Promise { - // Create ExtensionHost with ACP-specific configuration. + static async create({ + sessionId, + cwd, + connection, + options, + deps, + }: { + sessionId: string + cwd: string + connection: AgentSideConnection + options: AcpSessionOptions + deps: AcpSessionDependencies + }): Promise { const hostOptions: ExtensionHostOptions = { mode: options.mode, user: null, @@ -222,16 +194,14 @@ export class AcpSession implements IAcpSession { model: options.model, workspacePath: cwd, extensionPath: options.extensionPath, - // ACP mode: disable direct output, we stream through ACP. - disableOutput: true, - // Don't persist state - ACP clients manage their own sessions. - ephemeral: true, + disableOutput: true, // ACP mode: disable direct output, we stream through ACP. + ephemeral: true, // Don't persist state - ACP clients manage their own sessions. } const extensionHost = new ExtensionHost(hostOptions) await extensionHost.activate() - const session = new AcpSession(sessionId, extensionHost, connection, cwd, options.mode, deps) + const session = new AcpSession(sessionId, extensionHost, connection, cwd, options, deps) session.setupEventHandlers() return session @@ -365,13 +335,8 @@ export class AcpSession implements IAcpSession { */ setModel(modelId: string): void { this.currentModelId = modelId - - // Map model ID to extension settings - // The property is apiModelId for most providers - this.extensionHost.sendToExtension({ - type: "updateSettings", - updatedSettings: { apiModelId: modelId }, - }) + const updatedSettings = getProviderSettings(this.options.provider, this.options.apiKey, modelId) + this.extensionHost.sendToExtension({ type: "updateSettings", updatedSettings }) } /** @@ -403,10 +368,7 @@ export class AcpSession implements IAcpSession { */ async dispose(): Promise { this.cancel() - - // Clean up event handler listeners this.eventHandler.cleanup() - await this.extensionHost.dispose() } @@ -419,10 +381,10 @@ export class AcpSession implements IAcpSession { * * @returns Result indicating success or failure with error details. */ - private async sendUpdateDirect(update: SessionNotification["update"]): Promise> { + private async sendUpdate(update: SessionUpdate): Promise> { try { // Log the update being sent to ACP connection (commented out - too noisy) - // this.logger.info("Session", `OUT: ${JSON.stringify(update)}`) + this.logger.info("Session", `OUT: ${JSON.stringify(update)}`) await this.connection.sessionUpdate({ sessionId: this.sessionId, update }) return ok(undefined) } catch (error) { diff --git a/apps/cli/src/acp/types.ts b/apps/cli/src/acp/types.ts index 3a79fb64b3..414fdd57d5 100644 --- a/apps/cli/src/acp/types.ts +++ b/apps/cli/src/acp/types.ts @@ -1,76 +1,42 @@ -/** - * ACP Types for Mode and Model Pickers - * - * Extends the standard ACP types with model support for the Roo Code agent. - */ +import type { ModelInfo, SessionMode } from "@agentclientprotocol/sdk" -import type * as acp from "@agentclientprotocol/sdk" - -import { DEFAULT_FLAGS } from "@/types/constants.js" - -// ============================================================================= -// Model Types -// ============================================================================= - -/** - * Represents an available model in the ACP interface. - */ -export interface AcpModel { - /** Unique identifier for the model */ - modelId: string - /** Human-readable name */ - name: string - /** Optional description with details like pricing */ - description?: string -} - -/** - * State of available models and current selection. - */ -export interface AcpModelState { - /** List of available models */ - availableModels: AcpModel[] - /** Currently selected model ID */ - currentModelId: string -} - -// ============================================================================= -// Extended Response Types -// ============================================================================= - -/** - * Extended NewSessionResponse that includes model state. - * The standard ACP NewSessionResponse only includes sessionId and optional modes. - * We extend it with models for our implementation. - */ -export interface ExtendedNewSessionResponse extends acp.NewSessionResponse { - /** Model state for the session */ - models?: AcpModelState -} - -// ============================================================================= -// Default Constants -// ============================================================================= - -/** - * Default models available when API is not accessible. - * These map to Roo Code Cloud model tiers. - * The first model uses DEFAULT_FLAGS.model as the source of truth. - */ -export const DEFAULT_MODELS: AcpModel[] = [ - { - modelId: DEFAULT_FLAGS.model, - name: "Claude Sonnet 4.5", - description: "Best balance of speed and capability", - }, +export const DEFAULT_MODELS: ModelInfo[] = [ { modelId: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5", description: "Most capable for complex work", }, + { + modelId: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + description: "Best balance of speed and capability", + }, { modelId: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", description: "Fastest for quick answers", }, ] + +export const AVAILABLE_MODES: SessionMode[] = [ + { + id: "code", + name: "Code", + description: "Write, modify, and refactor code", + }, + { + id: "architect", + name: "Architect", + description: "Plan and design system architecture", + }, + { + id: "ask", + name: "Ask", + description: "Ask questions and get explanations", + }, + { + id: "debug", + name: "Debug", + description: "Debug issues and troubleshoot problems", + }, +] diff --git a/apps/cli/src/commands/acp/index.ts b/apps/cli/src/commands/acp/index.ts index 9be7b8b529..f416a21209 100644 --- a/apps/cli/src/commands/acp/index.ts +++ b/apps/cli/src/commands/acp/index.ts @@ -6,7 +6,7 @@ import * as acpSdk from "@agentclientprotocol/sdk" import { type SupportedProvider, DEFAULT_FLAGS } from "@/types/index.js" import { getDefaultExtensionPath } from "@/lib/utils/extension.js" -import { type RooCodeAgentOptions, RooCodeAgent, acpLog } from "@/acp/index.js" +import { RooCodeAgent, acpLog } from "@/acp/index.js" export interface AcpCommandOptions { extension?: string @@ -25,14 +25,6 @@ export async function runAcpServer(options: AcpCommandOptions): Promise { process.exit(1) } - const agentOptions: RooCodeAgentOptions = { - extensionPath, - provider: options.provider || DEFAULT_FLAGS.provider, - model: options.model || DEFAULT_FLAGS.model, - mode: options.mode || DEFAULT_FLAGS.mode, - apiKey: options.apiKey || process.env.OPENROUTER_API_KEY, - } - // Set up stdio streams for ACP communication. // Note: We write to stdout (agent -> client) and read from stdin (client -> agent). const stdout = Writable.toWeb(process.stdout) as WritableStream @@ -45,7 +37,17 @@ export async function runAcpServer(options: AcpCommandOptions): Promise { const connection = new acpSdk.AgentSideConnection((conn: acpSdk.AgentSideConnection) => { acpLog.info("Command", "Agent connection established") - agent = new RooCodeAgent(agentOptions, conn) + agent = new RooCodeAgent( + { + extensionPath, + provider: options.provider ?? DEFAULT_FLAGS.provider, + model: options.model || DEFAULT_FLAGS.model, + mode: options.mode || DEFAULT_FLAGS.mode, + apiKey: options.apiKey || process.env.OPENROUTER_API_KEY, + }, + conn, + ) + return agent }, stream) diff --git a/apps/cli/src/types/constants.ts b/apps/cli/src/types/constants.ts index 04abb90ad4..891ff87d00 100644 --- a/apps/cli/src/types/constants.ts +++ b/apps/cli/src/types/constants.ts @@ -4,7 +4,7 @@ export const DEFAULT_FLAGS = { mode: "code", reasoningEffort: "medium" as const, model: "anthropic/claude-opus-4.5", - provider: "openrouter", + provider: "openrouter" as const, } export const REASONING_EFFORTS = [...reasoningEffortsExtended, "unspecified", "disabled"]