feat(acp): add session/list and session/fork support (#7976)
This commit is contained in:
@@ -49,7 +49,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.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/anthropic": "2.0.57",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
|
||||
@@ -5,14 +5,21 @@ import {
|
||||
type AuthenticateRequest,
|
||||
type AuthMethod,
|
||||
type CancelNotification,
|
||||
type ForkSessionRequest,
|
||||
type ForkSessionResponse,
|
||||
type InitializeRequest,
|
||||
type InitializeResponse,
|
||||
type ListSessionsRequest,
|
||||
type ListSessionsResponse,
|
||||
type LoadSessionRequest,
|
||||
type NewSessionRequest,
|
||||
type PermissionOption,
|
||||
type PlanEntry,
|
||||
type PromptRequest,
|
||||
type ResumeSessionRequest,
|
||||
type ResumeSessionResponse,
|
||||
type Role,
|
||||
type SessionInfo,
|
||||
type SetSessionModelRequest,
|
||||
type SetSessionModeRequest,
|
||||
type SetSessionModeResponse,
|
||||
@@ -430,6 +437,11 @@ export namespace ACP {
|
||||
embeddedContext: true,
|
||||
image: true,
|
||||
},
|
||||
sessionCapabilities: {
|
||||
fork: {},
|
||||
list: {},
|
||||
resume: {},
|
||||
},
|
||||
},
|
||||
authMethods: [authMethod],
|
||||
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) {
|
||||
log.debug("process message", message)
|
||||
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
||||
|
||||
Reference in New Issue
Block a user