mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
Fix model switching
This commit is contained in:
parent
1b867d517b
commit
1150158d55
12 changed files with 394 additions and 787 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<string, AcpSession> = 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<acp.InitializeResponse> {
|
||||
acpLog.request("initialize", { protocolVersion: params.protocolVersion })
|
||||
|
||||
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||
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<NewSessionResponse> {
|
||||
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<SetSessionModeResponse | void> {
|
||||
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<SetSessionModelResponse | void> {
|
||||
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<AuthenticateResponse | void> {
|
||||
acpLog.request("authenticate", params)
|
||||
|
||||
/**
|
||||
* Authenticate with Roo Code Cloud.
|
||||
*/
|
||||
async authenticate(params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse | void> {
|
||||
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<ExtendedNewSessionResponse> {
|
||||
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<AcpModelState> {
|
||||
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<acp.PromptResponse> {
|
||||
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
||||
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<void> {
|
||||
async cancel(params: CancelNotification): Promise<void> {
|
||||
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<acp.SetSessionModeResponse | void> {
|
||||
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<void> {
|
||||
acpLog.info("Agent", `Disposing ${this.sessions.size} sessions`)
|
||||
const disposals = Array.from(this.sessions.values()).map((session) => session.dispose())
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<AcpModel[]> {
|
||||
// Return cached models if available
|
||||
async fetchAvailableModels(): Promise<ModelInfo[]> {
|
||||
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<AcpModelState> {
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof this.extensionHost.sendToExtension>[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<AcpSession> {
|
||||
// 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<AcpSession> {
|
||||
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<void> {
|
||||
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<Result<void>> {
|
||||
private async sendUpdate(update: SessionUpdate): Promise<Result<void>> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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<Uint8Array>
|
||||
|
|
@ -45,7 +37,17 @@ export async function runAcpServer(options: AcpCommandOptions): Promise<void> {
|
|||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue