|
@@ -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
|