feat(core): add session form service (#34855)
This commit is contained in:
parent
460cdc5aec
commit
7ebd344fa2
67 changed files with 7862 additions and 5223 deletions
|
|
@ -1 +1,4 @@
|
|||
delete process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]
|
||||
delete process.env["OTEL_EXPORTER_OTLP_HEADERS"]
|
||||
|
||||
await import("../test/server/httpapi-exercise/index")
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
|
||||
const rejectQuestion = async (request: { id: string }) => {
|
||||
questionRejected = true
|
||||
await input.client.v2.session.question.reject({ sessionID: input.sessionID, requestID: request.id }).catch(() => {})
|
||||
await input.client.question.reject({ requestID: request.id }).catch(() => {})
|
||||
}
|
||||
|
||||
const consume = async () => {
|
||||
|
|
@ -127,7 +127,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
await replyPermission(event.data)
|
||||
continue
|
||||
}
|
||||
if (event.type === "question.v2.asked" && submitted && event.data.sessionID === input.sessionID) {
|
||||
if (event.type === "question.asked" && submitted && event.data.sessionID === input.sessionID) {
|
||||
await rejectQuestion(event.data)
|
||||
continue
|
||||
}
|
||||
|
|
@ -408,11 +408,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
|
||||
const [permissions, questions] = await Promise.all([
|
||||
input.client.v2.session.permission.list({ sessionID: input.sessionID }).catch(() => undefined),
|
||||
input.client.v2.session.question.list({ sessionID: input.sessionID }).catch(() => undefined),
|
||||
input.client.question.list().catch(() => undefined),
|
||||
])
|
||||
await Promise.all([
|
||||
...(permissions?.data?.data ?? []).map(replyPermission),
|
||||
...(questions?.data?.data ?? []).map(rejectQuestion),
|
||||
...(questions?.data ?? []).filter((question) => question.sessionID === input.sessionID).map(rejectQuestion),
|
||||
])
|
||||
await completed
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -255,10 +255,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
return
|
||||
}
|
||||
|
||||
await ctx.sdk.v2.session.question.reply({
|
||||
sessionID: state.sessionID,
|
||||
await ctx.sdk.question.reply({
|
||||
requestID: next.requestID,
|
||||
questionV2Reply: { answers: next.answers ?? [] },
|
||||
answers: next.answers ?? [],
|
||||
})
|
||||
},
|
||||
onQuestionReject: async (next) => {
|
||||
|
|
@ -266,7 +265,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
return
|
||||
}
|
||||
|
||||
await ctx.sdk.v2.session.question.reject({ sessionID: state.sessionID, ...next })
|
||||
await ctx.sdk.question.reject(next)
|
||||
},
|
||||
onCycleVariant: () => {
|
||||
if (!state.model || state.variants.length === 0) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type {
|
|||
PermissionRequest,
|
||||
PermissionV2Request,
|
||||
QuestionRequest,
|
||||
QuestionV2Request,
|
||||
SessionMessage,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantTool,
|
||||
|
|
@ -133,15 +132,6 @@ function permission(request: PermissionV2Request): PermissionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
function question(request: QuestionV2Request): QuestionRequest {
|
||||
return {
|
||||
id: request.id,
|
||||
sessionID: request.sessionID,
|
||||
questions: request.questions,
|
||||
tool: request.tool,
|
||||
}
|
||||
}
|
||||
|
||||
function sessionID(event: RunV2Event) {
|
||||
return "sessionID" in event.data && typeof event.data.sessionID === "string" ? event.data.sessionID : undefined
|
||||
}
|
||||
|
|
@ -369,13 +359,13 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
{ throwOnError: true },
|
||||
),
|
||||
input.sdk.v2.session.permission.list({ sessionID: input.sessionID }, { throwOnError: true }),
|
||||
input.sdk.v2.session.question.list({ sessionID: input.sessionID }, { throwOnError: true }),
|
||||
input.sdk.question.list({ directory: input.directory }, { throwOnError: true }),
|
||||
input.sdk.v2.session.active({ throwOnError: true }),
|
||||
])
|
||||
const projected = messages.data.data.toReversed()
|
||||
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
||||
state.permissions = permissions.data.data.map(permission)
|
||||
state.questions = questions.data.data.map(question)
|
||||
state.questions = questions.data.filter((item) => item.sessionID === input.sessionID)
|
||||
syncBlockers()
|
||||
await subagents.hydrate({ messages: projected, active: active.data.data })
|
||||
const running = input.sessionID in active.data.data
|
||||
|
|
@ -544,12 +534,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
syncBlockers()
|
||||
return
|
||||
}
|
||||
if (event.type === "question.v2.asked") {
|
||||
if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(question(event.data))
|
||||
if (event.type === "question.asked") {
|
||||
if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(event.data)
|
||||
syncBlockers()
|
||||
return
|
||||
}
|
||||
if (event.type === "question.v2.replied" || event.type === "question.v2.rejected") {
|
||||
if (event.type === "question.replied" || event.type === "question.rejected") {
|
||||
state.questions = state.questions.filter((item) => item.id !== event.data.requestID)
|
||||
syncBlockers()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -25,9 +25,6 @@ import { SessionApi } from "./groups/session"
|
|||
import { SyncApi } from "./groups/sync"
|
||||
import { TuiApi } from "./groups/tui"
|
||||
import { WorkspaceApi } from "./groups/workspace"
|
||||
import { makeApi } from "@opencode-ai/protocol/api"
|
||||
import { LocationMiddleware } from "@opencode-ai/server/location"
|
||||
import { SessionLocationMiddleware } from "@opencode-ai/server/middleware/session-location"
|
||||
import { GlobalApi } from "./groups/global"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
|
|
@ -45,12 +42,6 @@ const EventSchema = Schema.Union([
|
|||
InstanceDisposed,
|
||||
]).annotate({ identifier: "Event" })
|
||||
|
||||
export const ServerApi = makeApi({
|
||||
definitions: EventManifest.Latest.values().toArray(),
|
||||
locationMiddleware: LocationMiddleware,
|
||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||
})
|
||||
|
||||
export const RootHttpApi = HttpApi.make("opencode-root")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(ControlPlaneApi)
|
||||
|
|
@ -80,7 +71,6 @@ export const OpenCodeHttpApi = HttpApi.make("opencode")
|
|||
.addHttpApi(RootHttpApi)
|
||||
.addHttpApi(EventApi)
|
||||
.addHttpApi(InstanceHttpApi)
|
||||
.addHttpApi(ServerApi)
|
||||
.addHttpApi(PtyConnectApi)
|
||||
.annotate(HttpApi.AdditionalSchemas, [
|
||||
EventSchema,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { MCP } from "@/mcp"
|
|||
import { McpAuth } from "@/mcp/auth"
|
||||
import { Permission } from "@/permission"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { PluginPtyEnvironment } from "@/plugin/pty-environment"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Project } from "@/project/project"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
|
|
@ -72,13 +71,11 @@ import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/
|
|||
import { serveUIEffect } from "@/server/shared/ui"
|
||||
import { ServerAuth } from "@/server/auth"
|
||||
import { InstanceHttpApi, RootHttpApi } from "./api"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
import { PublicApi } from "./public"
|
||||
import {
|
||||
authorizationLayer,
|
||||
authorizationRouterMiddleware,
|
||||
ptyConnectAuthorizationLayer,
|
||||
serverAuthorizationLayer,
|
||||
} from "./middleware/authorization"
|
||||
import { EventApi } from "./groups/event"
|
||||
import { PtyConnectApi } from "./groups/pty"
|
||||
|
|
@ -100,12 +97,10 @@ import { questionHandlers } from "./handlers/question"
|
|||
import { sessionHandlers } from "./handlers/session"
|
||||
import { syncHandlers } from "./handlers/sync"
|
||||
import { tuiHandlers } from "./handlers/tui"
|
||||
import { handlers } from "@opencode-ai/server/handlers"
|
||||
import { buildLocationServiceMap, LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { layer as locationLayer } from "@opencode-ai/server/location"
|
||||
import { sessionLocationLayer } from "@opencode-ai/server/middleware/session-location"
|
||||
import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
|
||||
import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
|
||||
import { workspaceHandlers } from "./handlers/workspace"
|
||||
import { instanceContextLayer } from "./middleware/instance-context"
|
||||
import { workspaceRoutingLayer } from "./middleware/workspace-routing"
|
||||
|
|
@ -137,7 +132,6 @@ const cors = (corsOptions?: CorsOptions) =>
|
|||
const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.layer))
|
||||
const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
|
||||
const ptyConnectHttpApiAuthLayer = ptyConnectAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
|
||||
const serverHttpApiAuthLayer = serverAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
|
||||
const workspaceRoutingLive = workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
|
||||
const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe(
|
||||
Layer.provide([controlHandlers, controlPlaneHandlers, globalHandlers]),
|
||||
|
|
@ -175,12 +169,6 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
|
|||
const instanceRoutes = instanceApiRoutes.pipe(
|
||||
Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]),
|
||||
)
|
||||
const serverRoutes = HttpApiBuilder.layer(Api).pipe(
|
||||
Layer.provide(handlers),
|
||||
Layer.provide(PluginPtyEnvironment.layer),
|
||||
Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]),
|
||||
)
|
||||
|
||||
// `OpenApi.fromApi` is non-trivial; defer until /doc is actually hit so
|
||||
// processes that never serve it (CLI, scripts) don't pay at module load.
|
||||
// `HttpServerResponse.jsonUnsafe` runs JSON.stringify eagerly, so caching
|
||||
|
|
@ -279,7 +267,6 @@ export function createRoutes(
|
|||
eventApiRoutes,
|
||||
ptyConnectApiRoutes,
|
||||
instanceRoutes,
|
||||
serverRoutes,
|
||||
docRoute,
|
||||
uiRoute,
|
||||
).pipe(
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ function sdk(input: {
|
|||
}),
|
||||
)
|
||||
spyOn(client.v2.session.permission, "list").mockImplementation(() => ok({ data: [] }))
|
||||
spyOn(client.v2.session.question, "list").mockImplementation(() => ok({ data: [] }))
|
||||
spyOn(client.question, "list").mockImplementation(() => ok([]))
|
||||
spyOn(client.v2.session, "active").mockImplementation(() => ok({ data: input.active?.() ?? {} }))
|
||||
spyOn(client.v2.session, "switchAgent").mockImplementation(() => ok(undefined))
|
||||
spyOn(client.v2.session, "switchModel").mockImplementation(() => ok(undefined))
|
||||
|
|
|
|||
|
|
@ -811,11 +811,6 @@ const scenarios: Scenario[] = [
|
|||
object(body.location)
|
||||
array(body.data)
|
||||
}),
|
||||
http.protected.get("/api/question/request", "v2.question.request.list").json(200, (body) => {
|
||||
object(body)
|
||||
object(body.location)
|
||||
array(body.data)
|
||||
}),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/permission", "v2.session.permission.create")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission create owner" }))
|
||||
|
|
@ -849,14 +844,6 @@ const scenarios: Scenario[] = [
|
|||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/question", "v2.session.question.list")
|
||||
.seeded((ctx) => ctx.session({ title: "Question list owner" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/question", { sessionID: ctx.state.id }),
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(200, data(array)),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/permission/{requestID}/reply", "v2.session.permission.reply")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission owner" }))
|
||||
|
|
@ -869,29 +856,6 @@ const scenarios: Scenario[] = [
|
|||
body: { reply: "once" },
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/question/{requestID}/reply", "v2.session.question.reply")
|
||||
.seeded((ctx) => ctx.session({ title: "Question reply owner" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/question/{requestID}/reply", {
|
||||
sessionID: ctx.state.id,
|
||||
requestID: "que_httpapi_missing",
|
||||
}),
|
||||
headers: ctx.headers(),
|
||||
body: { answers: [] },
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/question/{requestID}/reject", "v2.session.question.reject")
|
||||
.seeded((ctx) => ctx.session({ title: "Question reject owner" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/question/{requestID}/reject", {
|
||||
sessionID: ctx.state.id,
|
||||
requestID: "que_httpapi_missing",
|
||||
}),
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, (body) => {
|
||||
object(body)
|
||||
array(body.data)
|
||||
|
|
@ -1769,7 +1733,9 @@ const main = Effect.gen(function* () {
|
|||
const options = parseOptions(Bun.argv.slice(2))
|
||||
const modules = yield* Effect.promise(() => runtime())
|
||||
const effectRoutes = routeKeys(OpenApi.fromApi(modules.PublicApi))
|
||||
const selected = selectedScenarios(options, scenarios)
|
||||
const selected = selectedScenarios(options, scenarios).filter((scenario) =>
|
||||
effectRoutes.includes(routeKey(scenario)),
|
||||
)
|
||||
const missing = effectRoutes.filter((route) => !scenarios.some((scenario) => route === routeKey(scenario)))
|
||||
const extra = scenarios.filter((scenario) => !effectRoutes.includes(routeKey(scenario)))
|
||||
|
||||
|
|
|
|||
|
|
@ -137,11 +137,7 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||
test("preserves required request bodies for v2 mutations", () => {
|
||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||
|
||||
for (const path of [
|
||||
"/api/session/{sessionID}/prompt",
|
||||
"/api/session/{sessionID}/permission/{requestID}/reply",
|
||||
"/api/session/{sessionID}/question/{requestID}/reply",
|
||||
]) {
|
||||
for (const path of ["/api/session/{sessionID}/prompt", "/api/session/{sessionID}/permission/{requestID}/reply"]) {
|
||||
expect(spec.paths[path]?.post?.requestBody?.required, path).toBe(true)
|
||||
}
|
||||
})
|
||||
|
|
@ -292,15 +288,6 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||
"QuestionNotFoundError",
|
||||
)
|
||||
}
|
||||
for (const route of [
|
||||
["post", "/api/session/{sessionID}/question/{requestID}/reply"],
|
||||
["post", "/api/session/{sessionID}/question/{requestID}/reject"],
|
||||
] as const) {
|
||||
expect(componentNames(spec.paths[route[1]]?.[route[0]]?.responses?.["404"])).toEqual([
|
||||
"QuestionNotFoundError",
|
||||
"SessionNotFoundError",
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("documents MCP server not-found errors", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue