|
|
@@ -20,299 +20,321 @@ import {
|
|
|
} from "@agentclientprotocol/sdk"
|
|
|
import { Log } from "../util/log"
|
|
|
import { ACPSessionManager } from "./session"
|
|
|
-import type { ACPConfig } from "./types"
|
|
|
+import type { ACPConfig, ACPSessionState } 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"
|
|
|
import { MessageV2 } from "@/session/message-v2"
|
|
|
-import { Storage } from "@/storage/storage"
|
|
|
-import { Command } from "@/command"
|
|
|
-import { Agent as Agents } from "@/agent/agent"
|
|
|
-import { Permission } from "@/permission"
|
|
|
-import { SessionCompaction } from "@/session/compaction"
|
|
|
import { Config } from "@/config/config"
|
|
|
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)
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export class Agent implements ACPAgent {
|
|
|
- private sessionManager = new ACPSessionManager()
|
|
|
private connection: AgentSideConnection
|
|
|
private config: ACPConfig
|
|
|
+ private sdk: OpencodeClient
|
|
|
+ private sessionManager
|
|
|
|
|
|
- constructor(connection: AgentSideConnection, config: ACPConfig = {}) {
|
|
|
+ constructor(connection: AgentSideConnection, config: ACPConfig) {
|
|
|
this.connection = connection
|
|
|
this.config = config
|
|
|
- this.setupEventSubscriptions()
|
|
|
+ this.sdk = config.sdk
|
|
|
+ this.sessionManager = new ACPSessionManager(this.sdk)
|
|
|
}
|
|
|
|
|
|
- private setupEventSubscriptions() {
|
|
|
+ private setupEventSubscriptions(session: ACPSessionState) {
|
|
|
+ const sessionId = session.id
|
|
|
+ const directory = session.cwd
|
|
|
+
|
|
|
const options: PermissionOption[] = [
|
|
|
{ optionId: "once", kind: "allow_once", name: "Allow once" },
|
|
|
{ optionId: "always", kind: "allow_always", name: "Always allow" },
|
|
|
{ optionId: "reject", kind: "reject_once", name: "Reject" },
|
|
|
]
|
|
|
- Bus.subscribe(Permission.Event.Updated, async (event) => {
|
|
|
- const acpSession = this.sessionManager.get(event.properties.sessionID)
|
|
|
- if (!acpSession) return
|
|
|
- try {
|
|
|
- const permission = event.properties
|
|
|
- const res = await this.connection
|
|
|
- .requestPermission({
|
|
|
- sessionId: acpSession.id,
|
|
|
- toolCall: {
|
|
|
- toolCallId: permission.callID ?? permission.id,
|
|
|
- status: "pending",
|
|
|
- title: permission.title,
|
|
|
- rawInput: permission.metadata,
|
|
|
- kind: toToolKind(permission.type),
|
|
|
- locations: toLocations(permission.type, permission.metadata),
|
|
|
- },
|
|
|
- options,
|
|
|
- })
|
|
|
- .catch((error) => {
|
|
|
- log.error("failed to request permission from ACP", {
|
|
|
- error,
|
|
|
- permissionID: permission.id,
|
|
|
- sessionID: permission.sessionID,
|
|
|
- })
|
|
|
- Permission.respond({
|
|
|
- sessionID: permission.sessionID,
|
|
|
- permissionID: permission.id,
|
|
|
- response: "reject",
|
|
|
- })
|
|
|
- return
|
|
|
- })
|
|
|
- if (!res) return
|
|
|
- if (res.outcome.outcome !== "selected") {
|
|
|
- Permission.respond({
|
|
|
- sessionID: permission.sessionID,
|
|
|
- permissionID: permission.id,
|
|
|
- response: "reject",
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
- Permission.respond({
|
|
|
- sessionID: permission.sessionID,
|
|
|
- permissionID: permission.id,
|
|
|
- response: res.outcome.optionId as "once" | "always" | "reject",
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- if (!(err instanceof Permission.RejectedError)) {
|
|
|
- log.error("unexpected error when handling permission", { error: err })
|
|
|
- throw err
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- Bus.subscribe(MessageV2.Event.PartUpdated, async (event) => {
|
|
|
- const props = event.properties
|
|
|
- const { part } = props
|
|
|
- const acpSession = this.sessionManager.get(part.sessionID)
|
|
|
- if (!acpSession) return
|
|
|
-
|
|
|
- const message = await Storage.read<MessageV2.Info>([
|
|
|
- "message",
|
|
|
- part.sessionID,
|
|
|
- part.messageID,
|
|
|
- ]).catch(() => undefined)
|
|
|
- if (!message || message.role !== "assistant") return
|
|
|
-
|
|
|
- if (part.type === "tool") {
|
|
|
- switch (part.state.status) {
|
|
|
- case "pending":
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "tool_call",
|
|
|
- toolCallId: part.callID,
|
|
|
- title: part.tool,
|
|
|
- kind: toToolKind(part.tool),
|
|
|
- status: "pending",
|
|
|
- locations: [],
|
|
|
- rawInput: {},
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send tool pending to ACP", { error: err })
|
|
|
- })
|
|
|
- break
|
|
|
- case "running":
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "tool_call_update",
|
|
|
- toolCallId: part.callID,
|
|
|
- status: "in_progress",
|
|
|
- locations: toLocations(part.tool, part.state.input),
|
|
|
- rawInput: part.state.input,
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send tool in_progress to ACP", { error: err })
|
|
|
- })
|
|
|
- break
|
|
|
- case "completed":
|
|
|
- const kind = toToolKind(part.tool)
|
|
|
- const content: ToolCallContent[] = [
|
|
|
- {
|
|
|
- type: "content",
|
|
|
- content: {
|
|
|
- type: "text",
|
|
|
- text: part.state.output,
|
|
|
- },
|
|
|
- },
|
|
|
- ]
|
|
|
-
|
|
|
- if (kind === "edit") {
|
|
|
- const input = part.state.input
|
|
|
- const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
|
|
|
- const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
|
|
|
- const newText =
|
|
|
- typeof input["newString"] === "string"
|
|
|
- ? input["newString"]
|
|
|
- : typeof input["content"] === "string"
|
|
|
- ? input["content"]
|
|
|
- : ""
|
|
|
- content.push({
|
|
|
- type: "diff",
|
|
|
- path: filePath,
|
|
|
- oldText,
|
|
|
- newText,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- if (part.tool === "todowrite") {
|
|
|
- const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
|
|
|
- if (parsedTodos.success) {
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "plan",
|
|
|
- entries: parsedTodos.data.map((todo) => {
|
|
|
- const status: PlanEntry["status"] =
|
|
|
- todo.status === "cancelled"
|
|
|
- ? "completed"
|
|
|
- : (todo.status as PlanEntry["status"])
|
|
|
- return {
|
|
|
- priority: "medium",
|
|
|
- status,
|
|
|
- content: todo.content,
|
|
|
- }
|
|
|
- }),
|
|
|
- },
|
|
|
+ this.config.sdk.event.subscribe({ query: { directory } }).then(async (events) => {
|
|
|
+ for await (const event of events.stream) {
|
|
|
+ switch (event.type) {
|
|
|
+ case "permission.updated":
|
|
|
+ try {
|
|
|
+ const permission = event.properties
|
|
|
+ const res = await this.connection
|
|
|
+ .requestPermission({
|
|
|
+ sessionId,
|
|
|
+ toolCall: {
|
|
|
+ toolCallId: permission.callID ?? permission.id,
|
|
|
+ status: "pending",
|
|
|
+ title: permission.title,
|
|
|
+ rawInput: permission.metadata,
|
|
|
+ kind: toToolKind(permission.type),
|
|
|
+ locations: toLocations(permission.type, permission.metadata),
|
|
|
+ },
|
|
|
+ options,
|
|
|
+ })
|
|
|
+ .catch(async (error) => {
|
|
|
+ log.error("failed to request permission from ACP", {
|
|
|
+ error,
|
|
|
+ permissionID: permission.id,
|
|
|
+ sessionID: permission.sessionID,
|
|
|
})
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send session update for todo", { error: err })
|
|
|
+ await this.config.sdk.postSessionIdPermissionsPermissionId({
|
|
|
+ path: { id: permission.sessionID, permissionID: permission.id },
|
|
|
+ body: {
|
|
|
+ response: "reject",
|
|
|
+ },
|
|
|
+ query: { directory },
|
|
|
})
|
|
|
- } else {
|
|
|
- log.error("failed to parse todo output", { error: parsedTodos.error })
|
|
|
+ return
|
|
|
+ })
|
|
|
+ if (!res) return
|
|
|
+ if (res.outcome.outcome !== "selected") {
|
|
|
+ await this.config.sdk.postSessionIdPermissionsPermissionId({
|
|
|
+ path: { id: permission.sessionID, permissionID: permission.id },
|
|
|
+ body: {
|
|
|
+ response: "reject",
|
|
|
+ },
|
|
|
+ query: { directory },
|
|
|
+ })
|
|
|
+ return
|
|
|
}
|
|
|
+ await this.config.sdk.postSessionIdPermissionsPermissionId({
|
|
|
+ path: { id: permission.sessionID, permissionID: permission.id },
|
|
|
+ body: {
|
|
|
+ response: res.outcome.optionId as "once" | "always" | "reject",
|
|
|
+ },
|
|
|
+ query: { directory },
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ log.error("unexpected error when handling permission", { error: err })
|
|
|
+ } finally {
|
|
|
+ break
|
|
|
}
|
|
|
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "tool_call_update",
|
|
|
- toolCallId: part.callID,
|
|
|
- status: "completed",
|
|
|
- kind,
|
|
|
- content,
|
|
|
- title: part.state.title,
|
|
|
- rawOutput: {
|
|
|
- output: part.state.output,
|
|
|
- metadata: part.state.metadata,
|
|
|
+ case "message.part.updated":
|
|
|
+ log.info("message part updated", { event: event.properties })
|
|
|
+ try {
|
|
|
+ const props = event.properties
|
|
|
+ const { part } = props
|
|
|
+
|
|
|
+ const message = await this.config.sdk.session
|
|
|
+ .message({
|
|
|
+ throwOnError: true,
|
|
|
+ path: {
|
|
|
+ id: part.sessionID,
|
|
|
+ messageID: part.messageID,
|
|
|
},
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send tool completed to ACP", { error: err })
|
|
|
- })
|
|
|
- break
|
|
|
- case "error":
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "tool_call_update",
|
|
|
- toolCallId: part.callID,
|
|
|
- status: "failed",
|
|
|
- content: [
|
|
|
- {
|
|
|
- type: "content",
|
|
|
- content: {
|
|
|
- type: "text",
|
|
|
- text: part.state.error,
|
|
|
+ query: { directory },
|
|
|
+ })
|
|
|
+ .then((x) => x.data)
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("unexpected error when fetching message", { error: err })
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!message || message.info.role !== "assistant") return
|
|
|
+
|
|
|
+ if (part.type === "tool") {
|
|
|
+ switch (part.state.status) {
|
|
|
+ case "pending":
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "tool_call",
|
|
|
+ toolCallId: part.callID,
|
|
|
+ title: part.tool,
|
|
|
+ kind: toToolKind(part.tool),
|
|
|
+ status: "pending",
|
|
|
+ locations: [],
|
|
|
+ rawInput: {},
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send tool pending to ACP", { error: err })
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case "running":
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "tool_call_update",
|
|
|
+ toolCallId: part.callID,
|
|
|
+ status: "in_progress",
|
|
|
+ locations: toLocations(part.tool, part.state.input),
|
|
|
+ rawInput: part.state.input,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send tool in_progress to ACP", { error: err })
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case "completed":
|
|
|
+ const kind = toToolKind(part.tool)
|
|
|
+ const content: ToolCallContent[] = [
|
|
|
+ {
|
|
|
+ type: "content",
|
|
|
+ content: {
|
|
|
+ type: "text",
|
|
|
+ text: part.state.output,
|
|
|
+ },
|
|
|
},
|
|
|
- },
|
|
|
- ],
|
|
|
- rawOutput: {
|
|
|
- error: part.state.error,
|
|
|
- },
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send tool error to ACP", { error: err })
|
|
|
- })
|
|
|
- break
|
|
|
- }
|
|
|
- } else if (part.type === "text") {
|
|
|
- const delta = props.delta
|
|
|
- if (delta && part.synthetic !== true) {
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "agent_message_chunk",
|
|
|
- content: {
|
|
|
- type: "text",
|
|
|
- text: delta,
|
|
|
- },
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send text to ACP", { error: err })
|
|
|
- })
|
|
|
- }
|
|
|
- } else if (part.type === "reasoning") {
|
|
|
- const delta = props.delta
|
|
|
- if (delta) {
|
|
|
- await this.connection
|
|
|
- .sessionUpdate({
|
|
|
- sessionId: acpSession.id,
|
|
|
- update: {
|
|
|
- sessionUpdate: "agent_thought_chunk",
|
|
|
- content: {
|
|
|
- type: "text",
|
|
|
- text: delta,
|
|
|
- },
|
|
|
- },
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- log.error("failed to send reasoning to ACP", { error: err })
|
|
|
- })
|
|
|
+ ]
|
|
|
+
|
|
|
+ if (kind === "edit") {
|
|
|
+ const input = part.state.input
|
|
|
+ const filePath =
|
|
|
+ typeof input["filePath"] === "string" ? input["filePath"] : ""
|
|
|
+ const oldText =
|
|
|
+ typeof input["oldString"] === "string" ? input["oldString"] : ""
|
|
|
+ const newText =
|
|
|
+ typeof input["newString"] === "string"
|
|
|
+ ? input["newString"]
|
|
|
+ : typeof input["content"] === "string"
|
|
|
+ ? input["content"]
|
|
|
+ : ""
|
|
|
+ content.push({
|
|
|
+ type: "diff",
|
|
|
+ path: filePath,
|
|
|
+ oldText,
|
|
|
+ newText,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (part.tool === "todowrite") {
|
|
|
+ const parsedTodos = z
|
|
|
+ .array(Todo.Info)
|
|
|
+ .safeParse(JSON.parse(part.state.output))
|
|
|
+ if (parsedTodos.success) {
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "plan",
|
|
|
+ entries: parsedTodos.data.map((todo) => {
|
|
|
+ const status: PlanEntry["status"] =
|
|
|
+ todo.status === "cancelled"
|
|
|
+ ? "completed"
|
|
|
+ : (todo.status as PlanEntry["status"])
|
|
|
+ return {
|
|
|
+ priority: "medium",
|
|
|
+ status,
|
|
|
+ content: todo.content,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send session update for todo", { error: err })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ log.error("failed to parse todo output", { error: parsedTodos.error })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "tool_call_update",
|
|
|
+ toolCallId: part.callID,
|
|
|
+ status: "completed",
|
|
|
+ kind,
|
|
|
+ content,
|
|
|
+ title: part.state.title,
|
|
|
+ rawOutput: {
|
|
|
+ output: part.state.output,
|
|
|
+ metadata: part.state.metadata,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send tool completed to ACP", { error: err })
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case "error":
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "tool_call_update",
|
|
|
+ toolCallId: part.callID,
|
|
|
+ status: "failed",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "content",
|
|
|
+ content: {
|
|
|
+ type: "text",
|
|
|
+ text: part.state.error,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ rawOutput: {
|
|
|
+ error: part.state.error,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send tool error to ACP", { error: err })
|
|
|
+ })
|
|
|
+ break
|
|
|
+ }
|
|
|
+ } else if (part.type === "text") {
|
|
|
+ const delta = props.delta
|
|
|
+ if (delta && part.synthetic !== true) {
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "agent_message_chunk",
|
|
|
+ content: {
|
|
|
+ type: "text",
|
|
|
+ text: delta,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send text to ACP", { error: err })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else if (part.type === "reasoning") {
|
|
|
+ const delta = props.delta
|
|
|
+ if (delta) {
|
|
|
+ await this.connection
|
|
|
+ .sessionUpdate({
|
|
|
+ sessionId,
|
|
|
+ update: {
|
|
|
+ sessionUpdate: "agent_thought_chunk",
|
|
|
+ content: {
|
|
|
+ type: "text",
|
|
|
+ text: delta,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ log.error("failed to send reasoning to ACP", { error: err })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ break
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
@@ -364,19 +386,26 @@ export namespace ACP {
|
|
|
}
|
|
|
|
|
|
async newSession(params: NewSessionRequest) {
|
|
|
+ const directory = params.cwd
|
|
|
try {
|
|
|
- const model = await defaultModel(this.config)
|
|
|
- const session = await this.sessionManager.create(params.cwd, params.mcpServers, model)
|
|
|
+ const model = await defaultModel(this.config, directory)
|
|
|
+
|
|
|
+ // Store ACP session state
|
|
|
+ const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
|
|
|
+ const sessionId = state.id
|
|
|
+
|
|
|
+ log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
|
|
|
|
|
|
- log.info("creating_session", { mcpServers: params.mcpServers.length })
|
|
|
const load = await this.loadSession({
|
|
|
- cwd: params.cwd,
|
|
|
+ cwd: directory,
|
|
|
mcpServers: params.mcpServers,
|
|
|
- sessionId: session.id,
|
|
|
+ sessionId,
|
|
|
})
|
|
|
|
|
|
+ this.setupEventSubscriptions(state)
|
|
|
+
|
|
|
return {
|
|
|
- sessionId: session.id,
|
|
|
+ sessionId,
|
|
|
models: load.models,
|
|
|
modes: load.modes,
|
|
|
_meta: {},
|
|
|
@@ -393,26 +422,47 @@ export namespace ACP {
|
|
|
}
|
|
|
|
|
|
async loadSession(params: LoadSessionRequest) {
|
|
|
- const model = await defaultModel(this.config)
|
|
|
+ const directory = params.cwd
|
|
|
+ const model = await defaultModel(this.config, directory)
|
|
|
const sessionId = params.sessionId
|
|
|
|
|
|
- const providers = await Provider.list()
|
|
|
- const entries = Object.entries(providers).sort((a, b) => {
|
|
|
- const nameA = a[1].info.name.toLowerCase()
|
|
|
- const nameB = b[1].info.name.toLowerCase()
|
|
|
+ const providers = await this.sdk.config
|
|
|
+ .providers({ throwOnError: true, query: { directory } })
|
|
|
+ .then((x) => x.data.providers)
|
|
|
+ const entries = providers.sort((a, b) => {
|
|
|
+ const nameA = a.name.toLowerCase()
|
|
|
+ const nameB = b.name.toLowerCase()
|
|
|
if (nameA < nameB) return -1
|
|
|
if (nameA > nameB) return 1
|
|
|
return 0
|
|
|
})
|
|
|
- const availableModels = entries.flatMap(([providerID, provider]) => {
|
|
|
- const models = Provider.sort(Object.values(provider.info.models))
|
|
|
+ const availableModels = entries.flatMap((provider) => {
|
|
|
+ const models = Provider.sort(Object.values(provider.models))
|
|
|
return models.map((model) => ({
|
|
|
- modelId: `${providerID}/${model.id}`,
|
|
|
- name: `${provider.info.name}/${model.name}`,
|
|
|
+ modelId: `${provider.id}/${model.id}`,
|
|
|
+ name: `${provider.name}/${model.name}`,
|
|
|
}))
|
|
|
})
|
|
|
|
|
|
- const availableCommands = (await Command.list()).map((command) => ({
|
|
|
+ const agents = await this.config.sdk.app
|
|
|
+ .agents({
|
|
|
+ throwOnError: true,
|
|
|
+ query: {
|
|
|
+ directory,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .then((resp) => resp.data)
|
|
|
+
|
|
|
+ const commands = await this.config.sdk.command
|
|
|
+ .list({
|
|
|
+ throwOnError: true,
|
|
|
+ query: {
|
|
|
+ directory,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .then((resp) => resp.data)
|
|
|
+
|
|
|
+ const availableCommands = commands.map((command) => ({
|
|
|
name: command.name,
|
|
|
description: command.description ?? "",
|
|
|
}))
|
|
|
@@ -423,7 +473,7 @@ export namespace ACP {
|
|
|
description: "compact the session",
|
|
|
})
|
|
|
|
|
|
- const availableModes = (await Agents.list())
|
|
|
+ const availableModes = agents
|
|
|
.filter((agent) => agent.mode !== "subagent")
|
|
|
.map((agent) => ({
|
|
|
id: agent.name,
|
|
|
@@ -459,7 +509,18 @@ export namespace ACP {
|
|
|
|
|
|
await Promise.all(
|
|
|
Object.entries(mcpServers).map(async ([key, mcp]) => {
|
|
|
- await MCP.add(key, mcp)
|
|
|
+ await this.sdk.mcp
|
|
|
+ .add({
|
|
|
+ throwOnError: true,
|
|
|
+ query: { directory },
|
|
|
+ body: {
|
|
|
+ name: key,
|
|
|
+ config: mcp,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ log.error("failed to add mcp server", { name: key, error })
|
|
|
+ })
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
@@ -489,12 +550,8 @@ export namespace ACP {
|
|
|
|
|
|
async setSessionModel(params: SetSessionModelRequest) {
|
|
|
const session = this.sessionManager.get(params.sessionId)
|
|
|
- if (!session) {
|
|
|
- throw new Error(`Session not found: ${params.sessionId}`)
|
|
|
- }
|
|
|
|
|
|
- const parsed = Provider.parseModel(params.modelId)
|
|
|
- const model = await Provider.getModel(parsed.providerID, parsed.modelID)
|
|
|
+ const model = Provider.parseModel(params.modelId)
|
|
|
|
|
|
this.sessionManager.setModel(session.id, {
|
|
|
providerID: model.providerID,
|
|
|
@@ -507,31 +564,32 @@ export namespace ACP {
|
|
|
}
|
|
|
|
|
|
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
|
|
|
- const session = this.sessionManager.get(params.sessionId)
|
|
|
- if (!session) {
|
|
|
- throw new Error(`Session not found: ${params.sessionId}`)
|
|
|
- }
|
|
|
- await Agents.get(params.modeId).then((agent) => {
|
|
|
- if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
|
|
|
- })
|
|
|
+ this.sessionManager.get(params.sessionId)
|
|
|
+ await this.config.sdk.app
|
|
|
+ .agents({ throwOnError: true })
|
|
|
+ .then((x) => x.data)
|
|
|
+ .then((agent) => {
|
|
|
+ if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
|
|
|
+ })
|
|
|
this.sessionManager.setMode(params.sessionId, params.modeId)
|
|
|
}
|
|
|
|
|
|
async prompt(params: PromptRequest) {
|
|
|
const sessionID = params.sessionId
|
|
|
- const acpSession = this.sessionManager.get(sessionID)
|
|
|
- if (!acpSession) {
|
|
|
- throw new Error(`Session not found: ${sessionID}`)
|
|
|
- }
|
|
|
+ const session = this.sessionManager.get(sessionID)
|
|
|
+ const directory = session.cwd
|
|
|
|
|
|
- const current = acpSession.model
|
|
|
- const model = current ?? (await defaultModel(this.config))
|
|
|
+ const current = session.model
|
|
|
+ const model = current ?? (await defaultModel(this.config, directory))
|
|
|
if (!current) {
|
|
|
- this.sessionManager.setModel(acpSession.id, model)
|
|
|
+ this.sessionManager.setModel(session.id, model)
|
|
|
}
|
|
|
- const agent = acpSession.modeId ?? "build"
|
|
|
+ const agent = session.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 +603,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 +641,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,36 +658,50 @@ 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,
|
|
|
+ },
|
|
|
+ query: {
|
|
|
+ directory,
|
|
|
},
|
|
|
- parts,
|
|
|
- agent,
|
|
|
})
|
|
|
return done
|
|
|
}
|
|
|
|
|
|
- const command = await Command.get(cmd.name)
|
|
|
+ const command = await this.config.sdk.command
|
|
|
+ .list({ throwOnError: true, query: { directory } })
|
|
|
+ .then((x) => x.data.find((c) => c.name === 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,
|
|
|
+ },
|
|
|
+ query: {
|
|
|
+ directory,
|
|
|
+ },
|
|
|
})
|
|
|
return done
|
|
|
}
|
|
|
|
|
|
switch (cmd.name) {
|
|
|
case "compact":
|
|
|
- await SessionCompaction.run({
|
|
|
- sessionID,
|
|
|
- providerID: model.providerID,
|
|
|
- modelID: model.modelID,
|
|
|
+ await this.config.sdk.session.summarize({
|
|
|
+ path: { id: sessionID },
|
|
|
+ throwOnError: true,
|
|
|
+ query: {
|
|
|
+ directory,
|
|
|
+ },
|
|
|
})
|
|
|
break
|
|
|
}
|
|
|
@@ -636,7 +710,14 @@ export namespace ACP {
|
|
|
}
|
|
|
|
|
|
async cancel(params: CancelNotification) {
|
|
|
- SessionLock.abort(params.sessionId)
|
|
|
+ const session = this.sessionManager.get(params.sessionId)
|
|
|
+ await this.config.sdk.session.abort({
|
|
|
+ path: { id: params.sessionId },
|
|
|
+ throwOnError: true,
|
|
|
+ query: {
|
|
|
+ directory: session.cwd,
|
|
|
+ },
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -687,12 +768,15 @@ export namespace ACP {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async function defaultModel(config: ACPConfig) {
|
|
|
+ async function defaultModel(config: ACPConfig, cwd?: string) {
|
|
|
+ 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, query: { directory: cwd } })
|
|
|
+ .then((resp) => {
|
|
|
+ const cfg = resp.data
|
|
|
if (!cfg.model) return undefined
|
|
|
const parsed = Provider.parseModel(cfg.model)
|
|
|
return {
|