FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(acp): add ACP v2 draft support/features by mountaintopsolutions · Pull Request #44524 · anomalyco/opencode · GitHub

Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .json  (1) .lock  (1) .ts  (14) All 3 file types selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
40 changes: 2 additions & 38 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/opencode/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@agentclientprotocol/sdk": "1.3.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.166",
"@ai-sdk/anthropic": "3.0.111",
Expand Down
93 changes: 93 additions & 0 deletions packages/opencode/src/acp/agent-v2.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { agent, type AgentContext, type AgentApp, RequestError } from "@agentclientprotocol/sdk/experimental/v2"
import { Effect } from "effect"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import { Identifier } from "@/id/id"
import * as ACPError from "./error"
import * as ACPService from "./service"
import type { InitializeRequest as V1InitializeRequest } from "@agentclientprotocol/sdk"

function wrapClient(client: AgentContext): ACPService.ServiceConnection {
return {
sessionUpdate: (params) => client.notify("session/update", params),
requestPermission: (params) => client.request("session/request_permission", params) as Promise<never>,
}
}

// v2 handlers return v2-typed responses, but the service returns v1 types.
// The runtime shapes are compatible (v2 renames fields like id→configId,
// agentInfo→info). Promise<never> is assignable to any Promise<T>, bridging
// the type boundary without importing every v2 response type.
function run<A>(effect: Effect.Effect<A, ACPService.Error>): Promise<never> {
return Effect.runPromise(effect.pipe(Effect.mapError(ACPError.toRequestError))).catch((defect: unknown) => {
if (defect instanceof RequestError) throw defect
throw ACPError.toRequestError(ACPError.fromUnknownDefect(defect))
}) as Promise<never>
}

export function createV2App(sdk: OpencodeClient): AgentApp {
let service: ACPService.Interface | undefined

function serviceFor(client: AgentContext): ACPService.Interface {
if (!service) {
service = ACPService.make({ sdk, connection: wrapClient(client), v2: true })
}
return service
}

return agent({ name: "opencode" })
.onRequest("initialize", (ctx) => {
const service = serviceFor(ctx.client)
const v1Params = {
protocolVersion: ctx.params.protocolVersion,
clientInfo: ctx.params.info,
clientCapabilities: ctx.params.capabilities as unknown as undefined,
}
return run(service.initialize(v1Params as unknown as V1InitializeRequest))
})
.onRequest("auth/login", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.authenticate(ctx.params as unknown as Parameters<typeof service.authenticate>[0]))
})
.onRequest("auth/logout", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.logout() as Effect.Effect<Record<string, unknown>, ACPService.Error>)
})
.onRequest("session/new", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.newSession(ctx.params as unknown as Parameters<typeof service.newSession>[0]))
})
.onRequest("session/prompt", (ctx) => {
const service = serviceFor(ctx.client)
const params = {
...ctx.params,
messageId: Identifier.ascending("message"),
}
return run(service.prompt(params as unknown as Parameters<typeof service.prompt>[0]))
})
.onRequest("session/resume", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.resumeSession(ctx.params as unknown as Parameters<typeof service.resumeSession>[0]))
})
.onRequest("session/close", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.closeSession(ctx.params as unknown as Parameters<typeof service.closeSession>[0]))
})
.onRequest("session/fork", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.forkSession(ctx.params as unknown as Parameters<typeof service.forkSession>[0]))
})
.onRequest("session/list", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.listSessions(ctx.params as unknown as Parameters<typeof service.listSessions>[0]))
})
.onRequest("session/set_config_option", (ctx) => {
const service = serviceFor(ctx.client)
return run(
service.setSessionConfigOption(ctx.params as unknown as Parameters<typeof service.setSessionConfigOption>[0]),
)
})
.onNotification("session/cancel", (ctx) => {
if (!service) return
return run(service.cancel(ctx.params as unknown as Parameters<typeof service.cancel>[0]))
})
}
119 changes: 115 additions & 4 deletions packages/opencode/src/acp/agent.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
RequestError,
agent,
type Agent as ACPAgent,
type AgentSideConnection,
type AuthenticateRequest,
Expand All @@ -13,18 +14,18 @@ import {
type PromptRequest,
type ResumeSessionRequest,
type SetSessionConfigOptionRequest,
type SetSessionModelRequest,
type SetSessionModeRequest,
} from "@agentclientprotocol/sdk"
import { agentProtocolRouter } from "@agentclientprotocol/sdk/experimental/v2"
import { Effect } from "effect"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import * as ACPError from "./error"
import * as ACPService from "./service"

export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
export function init({ sdk: _sdk, v2 }: { sdk: OpencodeClient; v2?: boolean }) {
return {
create: (connection: AgentSideConnection) => {
return new Agent(ACPService.make({ sdk: _sdk, connection }))
return new Agent(ACPService.make({ sdk: _sdk, connection, v2 }))
},
}
}
Expand Down Expand Up @@ -72,7 +73,7 @@ export class Agent implements ACPAgent {
return run(this.service.setSessionMode(params))
}

unstable_setSessionModel(params: SetSessionModelRequest) {
unstable_setSessionModel(params: { sessionId: string; modelId: string }) {
return run(this.service.setSessionModel(params))
}

Expand All @@ -83,6 +84,27 @@ export class Agent implements ACPAgent {
cancel(params: CancelNotification) {
return run(this.service.cancel(params))
}

// v2 renames `authenticate` to `auth/login` and adds `auth/logout`. SDK 0.21
// has no dispatch case for these, so they arrive via the extension method hook.
extMethod(method: string, params: Record<string, unknown>) {
if (method === "auth/login") {
return run(
this.service.authenticate(params as unknown as AuthenticateRequest) as Effect.Effect<
Record<string, unknown>,
ACPService.Error
>,
)
}
if (method === "auth/logout") {
return run(this.service.logout() as Effect.Effect<Record<string, unknown>, ACPService.Error>)
}
return Promise.reject(new RequestError(-32601, `Method not found: ${method}`))
}

extNotification(method: string, _params: Record<string, unknown>) {
return Promise.reject(new RequestError(-32601, `Notification not found: ${method}`))
}
}

function run<A>(effect: Effect.Effect<A, ACPService.Error>) {
Expand All @@ -92,4 +114,93 @@ function run<A>(effect: Effect.Effect<A, ACPService.Error>) {
})
}

function wrapClient(client: {
notify: (m: string, p: unknown) => Promise<void>
request: (m: string, p: unknown) => Promise<unknown>
}): ACPService.ServiceConnection {
return {
sessionUpdate: (params) => client.notify("session/update", params),
requestPermission: (params) => client.request("session/request_permission", params) as Promise<never>,
}
}

export function createV1App(sdk: OpencodeClient) {
let service: ACPService.Interface | undefined

function serviceFor(
client: object & {
notify: (m: string, p: unknown) => Promise<void>
request: (m: string, p: unknown) => Promise<unknown>
},
): ACPService.Interface {
if (!service) {
service = ACPService.make({ sdk, connection: wrapClient(client), v2: false })
}
return service
}

return agent({ name: "opencode" })
.onRequest("initialize", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.initialize(ctx.params))
})
.onRequest("authenticate", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.authenticate(ctx.params))
})
.onRequest("logout", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.logout() as Effect.Effect<Record<string, unknown>, ACPService.Error>)
})
.onRequest("session/new", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.newSession(ctx.params))
})
.onRequest("session/load", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.loadSession(ctx.params))
})
.onRequest("session/list", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.listSessions(ctx.params))
})
.onRequest("session/resume", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.resumeSession(ctx.params))
})
.onRequest("session/close", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.closeSession(ctx.params))
})
.onRequest("session/fork", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.forkSession(ctx.params))
})
.onRequest("session/set_mode", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.setSessionMode(ctx.params))
})
.onRequest("session/set_config_option", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.setSessionConfigOption(ctx.params))
})
.onRequest("session/prompt", (ctx) => {
const service = serviceFor(ctx.client)
return run(service.prompt(ctx.params))
})
.onNotification("session/cancel", (ctx) => {
if (!service) return
return run(service.cancel(ctx.params))
})
}

export function createRouter(sdk: OpencodeClient, v2: boolean) {
const router = agentProtocolRouter().withV1(createV1App(sdk))
if (v2) {
// Lazy import to avoid loading v2 module when flag is off
return import("./agent-v2").then((mod) => router.withV2(mod.createV2App(sdk)))
}
return Promise.resolve(router)
}

export * as ACP from "./agent"
10 changes: 10 additions & 0 deletions packages/opencode/src/acp/config-option.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,13 @@ function selectVariant(variant: string | undefined, variants: readonly string[])
if (variants.includes(DEFAULT_VARIANT_VALUE)) return DEFAULT_VARIANT_VALUE
return variants[0]
}

// v2 renames the config option identifier field from `id` to `configId`.
// Applied at the boundary when responding to v2 clients. The v1 SDK types
// only know `id`, so the v2 shape is cast through `unknown`.
export function toV2ConfigOptions(options: readonly SessionConfigOption[]): SessionConfigOption[] {
return options.map((option) => {
const { id, ...rest } = option
return { configId: id, ...rest } as unknown as SessionConfigOption
})
}
16 changes: 15 additions & 1 deletion packages/opencode/src/acp/content.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ export type ReplayPart =
text: string
synthetic?: boolean
ignored?: boolean
_meta?: Record<string, unknown> | null
}
| {
type: "file"
url: string
mime: string
filename?: string
_meta?: Record<string, unknown> | null
}
| {
type: "reasoning"
text: string
_meta?: Record<string, unknown> | null
}

export function promptContentToParts(content: readonly ContentBlock[]): PromptPart[] {
return content.flatMap(contentBlockToParts)
}

export function contentBlockToParts(block: ContentBlock): PromptPart[] {
const meta = "_meta" in block && block._meta ? block._meta : undefined
switch (block.type) {
case "text":
return [
Expand Down Expand Up @@ -112,6 +116,9 @@ export function contentBlockToParts(block: ContentBlock): PromptPart[] {
return []

default:
// v2 extensibility: unknown content block types are rendered as a text
// fallback so the user's message content is not silently lost.
if (meta) return [{ type: "text", text: JSON.stringify(block) }]
return []
}
}
Expand All @@ -131,6 +138,7 @@ export function partToContentChunks(part: ReplayPart): ContentChunk[] {
text: part.text,
...partAudience(part),
},
...(part._meta ? { _meta: part._meta } : {}),
},
]

Expand All @@ -145,6 +153,7 @@ export function partToContentChunks(part: ReplayPart): ContentChunk[] {
type: "text",
text: part.text,
},
...(part._meta ? { _meta: part._meta } : {}),
},
]
}
Expand All @@ -153,7 +162,11 @@ export function partToContentChunks(part: ReplayPart): ContentChunk[] {
function resourceLinkToPart(link: ResourceLink): PromptPart {
const parsed = uriToFilePart(link.uri, link.mimeType ?? "text/plain", link.name)
if (parsed.type === "file") return parsed
return { type: "text", text: parsed.text }
return {
type: "text",
text: parsed.text,
...(link._meta ? { metadata: { _meta: link._meta } } : {}),
}
}

function uriToFilePart(
Expand Down Expand Up @@ -197,6 +210,7 @@ function filePartToContentChunks(part: Extract<ReplayPart, { type: "file" }>): C
name: part.filename ?? "file",
mimeType: part.mime,
},
...(part._meta ? { _meta: part._meta } : {}),
},
]
}
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL