feat(acp): add session/list and session/fork support (#7976)
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -263,7 +263,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.12.0",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.73",
|
"@ai-sdk/amazon-bedrock": "3.0.73",
|
||||||
"@ai-sdk/anthropic": "2.0.57",
|
"@ai-sdk/anthropic": "2.0.57",
|
||||||
"@ai-sdk/azure": "2.0.91",
|
"@ai-sdk/azure": "2.0.91",
|
||||||
@@ -554,7 +554,7 @@
|
|||||||
|
|
||||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.12.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="],
|
||||||
|
|
||||||
@@ -3978,8 +3978,6 @@
|
|||||||
|
|
||||||
"@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
"@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||||
|
|
||||||
"@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.12.0",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.73",
|
"@ai-sdk/amazon-bedrock": "3.0.73",
|
||||||
"@ai-sdk/anthropic": "2.0.57",
|
"@ai-sdk/anthropic": "2.0.57",
|
||||||
"@ai-sdk/azure": "2.0.91",
|
"@ai-sdk/azure": "2.0.91",
|
||||||
|
|||||||
@@ -5,14 +5,21 @@ import {
|
|||||||
type AuthenticateRequest,
|
type AuthenticateRequest,
|
||||||
type AuthMethod,
|
type AuthMethod,
|
||||||
type CancelNotification,
|
type CancelNotification,
|
||||||
|
type ForkSessionRequest,
|
||||||
|
type ForkSessionResponse,
|
||||||
type InitializeRequest,
|
type InitializeRequest,
|
||||||
type InitializeResponse,
|
type InitializeResponse,
|
||||||
|
type ListSessionsRequest,
|
||||||
|
type ListSessionsResponse,
|
||||||
type LoadSessionRequest,
|
type LoadSessionRequest,
|
||||||
type NewSessionRequest,
|
type NewSessionRequest,
|
||||||
type PermissionOption,
|
type PermissionOption,
|
||||||
type PlanEntry,
|
type PlanEntry,
|
||||||
type PromptRequest,
|
type PromptRequest,
|
||||||
|
type ResumeSessionRequest,
|
||||||
|
type ResumeSessionResponse,
|
||||||
type Role,
|
type Role,
|
||||||
|
type SessionInfo,
|
||||||
type SetSessionModelRequest,
|
type SetSessionModelRequest,
|
||||||
type SetSessionModeRequest,
|
type SetSessionModeRequest,
|
||||||
type SetSessionModeResponse,
|
type SetSessionModeResponse,
|
||||||
@@ -430,6 +437,11 @@ export namespace ACP {
|
|||||||
embeddedContext: true,
|
embeddedContext: true,
|
||||||
image: true,
|
image: true,
|
||||||
},
|
},
|
||||||
|
sessionCapabilities: {
|
||||||
|
fork: {},
|
||||||
|
list: {},
|
||||||
|
resume: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
agentInfo: {
|
agentInfo: {
|
||||||
@@ -540,6 +552,141 @@ export namespace ACP {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unstable_listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
||||||
|
try {
|
||||||
|
const cursor = params.cursor ? Number(params.cursor) : undefined
|
||||||
|
const limit = 100
|
||||||
|
|
||||||
|
const sessions = await this.sdk.session
|
||||||
|
.list(
|
||||||
|
{
|
||||||
|
directory: params.cwd ?? undefined,
|
||||||
|
roots: true,
|
||||||
|
},
|
||||||
|
{ throwOnError: true },
|
||||||
|
)
|
||||||
|
.then((x) => x.data ?? [])
|
||||||
|
|
||||||
|
const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||||
|
const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
|
||||||
|
const page = filtered.slice(0, limit)
|
||||||
|
|
||||||
|
const entries: SessionInfo[] = page.map((session) => ({
|
||||||
|
sessionId: session.id,
|
||||||
|
cwd: session.directory,
|
||||||
|
title: session.title,
|
||||||
|
updatedAt: new Date(session.time.updated).toISOString(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const last = page[page.length - 1]
|
||||||
|
const next = filtered.length > limit && last ? String(last.time.updated) : undefined
|
||||||
|
|
||||||
|
const response: ListSessionsResponse = {
|
||||||
|
sessions: entries,
|
||||||
|
}
|
||||||
|
if (next) response.nextCursor = next
|
||||||
|
return response
|
||||||
|
} catch (e) {
|
||||||
|
const error = MessageV2.fromError(e, {
|
||||||
|
providerID: this.config.defaultModel?.providerID ?? "unknown",
|
||||||
|
})
|
||||||
|
if (LoadAPIKeyError.isInstance(error)) {
|
||||||
|
throw RequestError.authRequired()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
||||||
|
const directory = params.cwd
|
||||||
|
const mcpServers = params.mcpServers ?? []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = await defaultModel(this.config, directory)
|
||||||
|
|
||||||
|
const forked = await this.sdk.session
|
||||||
|
.fork(
|
||||||
|
{
|
||||||
|
sessionID: params.sessionId,
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
{ throwOnError: true },
|
||||||
|
)
|
||||||
|
.then((x) => x.data)
|
||||||
|
|
||||||
|
if (!forked) {
|
||||||
|
throw new Error("Fork session returned no data")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = forked.id
|
||||||
|
await this.sessionManager.load(sessionId, directory, mcpServers, model)
|
||||||
|
|
||||||
|
log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
|
||||||
|
|
||||||
|
const mode = await this.loadSessionMode({
|
||||||
|
cwd: directory,
|
||||||
|
mcpServers,
|
||||||
|
sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = await this.sdk.session
|
||||||
|
.messages(
|
||||||
|
{
|
||||||
|
sessionID: sessionId,
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
{ throwOnError: true },
|
||||||
|
)
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch((err) => {
|
||||||
|
log.error("unexpected error when fetching message", { error: err })
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const msg of messages ?? []) {
|
||||||
|
log.debug("replay message", msg)
|
||||||
|
await this.processMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode
|
||||||
|
} catch (e) {
|
||||||
|
const error = MessageV2.fromError(e, {
|
||||||
|
providerID: this.config.defaultModel?.providerID ?? "unknown",
|
||||||
|
})
|
||||||
|
if (LoadAPIKeyError.isInstance(error)) {
|
||||||
|
throw RequestError.authRequired()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
|
||||||
|
const directory = params.cwd
|
||||||
|
const sessionId = params.sessionId
|
||||||
|
const mcpServers = params.mcpServers ?? []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const model = await defaultModel(this.config, directory)
|
||||||
|
await this.sessionManager.load(sessionId, directory, mcpServers, model)
|
||||||
|
|
||||||
|
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
|
||||||
|
|
||||||
|
return this.loadSessionMode({
|
||||||
|
cwd: directory,
|
||||||
|
mcpServers,
|
||||||
|
sessionId,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
const error = MessageV2.fromError(e, {
|
||||||
|
providerID: this.config.defaultModel?.providerID ?? "unknown",
|
||||||
|
})
|
||||||
|
if (LoadAPIKeyError.isInstance(error)) {
|
||||||
|
throw RequestError.authRequired()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async processMessage(message: SessionMessageResponse) {
|
private async processMessage(message: SessionMessageResponse) {
|
||||||
log.debug("process message", message)
|
log.debug("process message", message)
|
||||||
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
||||||
|
|||||||
Reference in New Issue
Block a user