This commit is contained in:
Aiden Cline 2025-11-07 11:39:10 -06:00
parent afb831c93c
commit d4b7c4a024
4 changed files with 95 additions and 47 deletions

View file

@ -22,7 +22,6 @@ import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
import { Provider } from "../provider/provider"
import { SessionPrompt } from "../session/prompt"
import { Installation } from "@/installation"
import { SessionLock } from "@/session/lock"
import { Bus } from "@/bus"
@ -37,18 +36,19 @@ import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
import type { OpencodeClient } from "@opencode-ai/sdk"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
export async function init() {
const model = await defaultModel({})
export async function init({ sdk }: { sdk: OpencodeClient }) {
const model = await defaultModel({ sdk })
return {
create: (connection: AgentSideConnection, config: ACPConfig) => {
if (!config.defaultModel) {
config.defaultModel = model
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
if (!fullConfig.defaultModel) {
fullConfig.defaultModel = model
}
return new Agent(connection, config)
return new Agent(connection, fullConfig)
},
}
}
@ -57,10 +57,12 @@ export namespace ACP {
private sessionManager = new ACPSessionManager()
private connection: AgentSideConnection
private config: ACPConfig
private sdk: ACPConfig["sdk"]
constructor(connection: AgentSideConnection, config: ACPConfig = {}) {
constructor(connection: AgentSideConnection, config: ACPConfig) {
this.connection = connection
this.config = config
this.sdk = config.sdk
this.setupEventSubscriptions()
}
@ -366,17 +368,36 @@ export namespace ACP {
async newSession(params: NewSessionRequest) {
try {
const model = await defaultModel(this.config)
const session = await this.sessionManager.create(params.cwd, params.mcpServers, model)
log.info("creating_session", { mcpServers: params.mcpServers.length })
// Create session via SDK
const result = await this.sdk.session.create({
body: {
title: `ACP Session ${crypto.randomUUID()}`,
},
query: {
directory: params.cwd,
},
})
if (!result.data?.id) {
throw new Error("Failed to create session")
}
const sessionId = result.data.id
// Store ACP session state
await this.sessionManager.create(sessionId, params.cwd, params.mcpServers, model)
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
const load = await this.loadSession({
cwd: params.cwd,
mcpServers: params.mcpServers,
sessionId: session.id,
sessionId,
})
return {
sessionId: session.id,
sessionId,
models: load.models,
modes: load.modes,
_meta: {},
@ -531,7 +552,10 @@ export namespace ACP {
}
const agent = acpSession.modeId ?? "build"
const parts: SessionPrompt.PromptInput["parts"] = []
const parts: Array<
| { type: "text"; text: string }
| { type: "file"; url: string; filename: string; mime: string }
> = []
for (const part of params.prompt) {
switch (part.type) {
case "text":
@ -545,12 +569,14 @@ export namespace ACP {
parts.push({
type: "file",
url: `data:${part.mimeType};base64,${part.data}`,
filename: "image",
mime: part.mimeType,
})
} else if (part.uri && part.uri.startsWith("http:")) {
parts.push({
type: "file",
url: part.uri,
filename: "image",
mime: part.mimeType,
})
}
@ -581,7 +607,7 @@ export namespace ACP {
const cmd = (() => {
const text = parts
.filter((p) => p.type === "text")
.filter((p): p is { type: "text"; text: string } => p.type === "text")
.map((p) => p.text)
.join("")
.trim()
@ -598,26 +624,30 @@ export namespace ACP {
}
if (!cmd) {
await SessionPrompt.prompt({
sessionID,
model: {
providerID: model.providerID,
modelID: model.modelID,
await this.sdk.session.prompt({
path: { id: sessionID },
body: {
model: {
providerID: model.providerID,
modelID: model.modelID,
},
parts,
agent,
},
parts,
agent,
})
return done
}
const command = await Command.get(cmd.name)
if (command) {
await SessionPrompt.command({
sessionID,
command: command.name,
arguments: cmd.args,
model: model.providerID + "/" + model.modelID,
agent,
await this.sdk.session.command({
path: { id: sessionID },
body: {
command: command.name,
arguments: cmd.args,
model: model.providerID + "/" + model.modelID,
agent,
},
})
return done
}
@ -688,11 +718,14 @@ export namespace ACP {
}
async function defaultModel(config: ACPConfig) {
const sdk = config.sdk
const configured = config.defaultModel
if (configured) return configured
const model = await Config.get()
.then((cfg) => {
const model = await sdk.config
.get({ throwOnError: true })
.then((resp) => {
const cfg = resp.data
if (!cfg.model) return undefined
const parsed = Provider.parseModel(cfg.model)
return {

View file

@ -1,5 +1,4 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import { Session } from "../session"
import { Provider } from "../provider/provider"
import type { ACPSessionState } from "./types"
@ -7,17 +6,15 @@ export class ACPSessionManager {
private sessions = new Map<string, ACPSessionState>()
async create(
sessionId: string,
cwd: string,
mcpServers: McpServer[],
model?: ACPSessionState["model"],
): Promise<ACPSessionState> {
const session = await Session.create({ title: `ACP Session ${crypto.randomUUID()}` })
const sessionId = session.id
const resolvedModel = model ?? (await Provider.defaultModel())
const state: ACPSessionState = {
id: sessionId,
parentId: session.parentID,
cwd,
mcpServers,
createdAt: new Date(),
@ -33,10 +30,6 @@ export class ACPSessionManager {
}
async remove(sessionId: string) {
const state = this.sessions.get(sessionId)
if (!state) return
await Session.remove(sessionId).catch(() => {})
this.sessions.delete(sessionId)
}

View file

@ -1,8 +1,8 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import type { OpencodeClient } from "@opencode-ai/sdk"
export interface ACPSessionState {
id: string
parentId?: string
cwd: string
mcpServers: McpServer[]
createdAt: Date
@ -14,6 +14,7 @@ export interface ACPSessionState {
}
export interface ACPConfig {
sdk: OpencodeClient
defaultModel?: {
providerID: string
modelID: string

View file

@ -3,6 +3,8 @@ import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
import { Server } from "@/server/server"
import { createOpencodeClient } from "@opencode-ai/sdk"
const log = Log.create({ service: "acp-command" })
@ -17,15 +19,34 @@ export const AcpCommand = cmd({
command: "acp",
describe: "Start ACP (Agent Client Protocol) server",
builder: (yargs) => {
return yargs.option("cwd", {
describe: "working directory",
type: "string",
default: process.cwd(),
})
return yargs
.option("cwd", {
describe: "working directory",
type: "string",
default: process.cwd(),
})
.option("port", {
type: "number",
describe: "port to listen on",
default: 0,
})
.option("hostname", {
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
})
},
handler: async (opts) => {
if (opts.cwd) process.chdir(opts["cwd"])
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
const server = Server.listen({
port: args.port,
hostname: args.hostname,
})
const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`,
})
const input = new WritableStream<Uint8Array>({
write(chunk) {
return new Promise<void>((resolve, reject) => {
@ -50,10 +71,10 @@ export const AcpCommand = cmd({
})
const stream = ndJsonStream(input, output)
const agent = await ACP.init()
const agent = await ACP.init({ sdk })
new AgentSideConnection((conn) => {
return agent.create(conn, {})
return agent.create(conn, { sdk })
}, stream)
log.info("setup connection")