refactor(server): canonicalize service API (#31049)

This commit is contained in:
Dax 2026-06-06 23:27:28 -04:00 committed by GitHub
commit fe0c4f8c74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
388 changed files with 7075 additions and 4064 deletions

View file

@ -2,12 +2,11 @@ import { AgentV2 } from "@opencode-ai/core/agent"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const AgentGroup = HttpApiGroup.make("v2.agent")
export const AgentGroup = HttpApiGroup.make("server.agent")
.add(
HttpApiEndpoint.get("agents", "/api/agent", {
HttpApiEndpoint.get("agent.list", "/api/agent", {
query: LocationQuery,
success: Location.response(Schema.Array(AgentV2.Info)),
})
@ -15,10 +14,9 @@ export const AgentGroup = HttpApiGroup.make("v2.agent")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.agent.list",
summary: "List v2 agents",
description: "Retrieve currently registered v2 agents.",
summary: "List agents",
description: "Retrieve currently registered agents.",
}),
),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -2,12 +2,11 @@ import { CommandV2 } from "@opencode-ai/core/command"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const CommandGroup = HttpApiGroup.make("v2.command")
export const CommandGroup = HttpApiGroup.make("server.command")
.add(
HttpApiEndpoint.get("commands", "/api/command", {
HttpApiEndpoint.get("command.list", "/api/command", {
query: LocationQuery,
success: Location.response(Schema.Array(CommandV2.Info)),
})
@ -15,16 +14,15 @@ export const CommandGroup = HttpApiGroup.make("v2.command")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.command.list",
summary: "List v2 commands",
description: "Retrieve currently registered v2 commands.",
summary: "List commands",
description: "Retrieve currently registered commands.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 commands",
description: "Experimental v2 command routes.",
title: "commands",
description: "Experimental command routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -2,8 +2,7 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
const Event = Schema.Struct({
id: EventV2.ID,
@ -14,9 +13,9 @@ const Event = Schema.Struct({
data: Schema.Unknown,
})
export const EventGroup = HttpApiGroup.make("v2.event")
export const EventGroup = HttpApiGroup.make("server.event")
.add(
HttpApiEndpoint.get("events", "/api/event", {
HttpApiEndpoint.get("event.subscribe", "/api/event", {
query: LocationQuery,
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
})
@ -24,13 +23,12 @@ export const EventGroup = HttpApiGroup.make("v2.event")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.event.subscribe",
summary: "Subscribe to v2 events",
description: "Subscribe to native EventV2 payloads for a location.",
summary: "Subscribe to events",
description: "Subscribe to native event payloads for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "v2 events", description: "Experimental v2 event stream route." }))
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
.middleware(LocationMiddleware)
export type Event = typeof Event.Type

View file

@ -3,8 +3,7 @@ import { Location } from "@opencode-ai/core/location"
import { RelativePath } from "@opencode-ai/core/schema"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
const ReadQuery = Schema.Struct({
...LocationQuery.fields,
@ -18,9 +17,9 @@ const ListQuery = Schema.Struct({
reference: Schema.String.pipe(Schema.optional),
})
export const FileSystemGroup = HttpApiGroup.make("v2.fs")
export const FileSystemGroup = HttpApiGroup.make("server.fs")
.add(
HttpApiEndpoint.get("read", "/api/fs/read", {
HttpApiEndpoint.get("fs.read", "/api/fs/read", {
query: ReadQuery,
success: Location.response(FileSystem.Content),
})
@ -34,7 +33,7 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
),
)
.add(
HttpApiEndpoint.get("list", "/api/fs/list", {
HttpApiEndpoint.get("fs.list", "/api/fs/list", {
query: ListQuery,
success: Location.response(Schema.Array(FileSystem.Entry)),
})
@ -49,9 +48,8 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
)
.annotateMerge(
OpenApi.annotations({
title: "v2 filesystem",
description: "Experimental v2 location-scoped filesystem routes.",
title: "filesystem",
description: "Experimental location-scoped filesystem routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -0,0 +1,15 @@
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const HealthGroup = HttpApiGroup.make("server.health")
.add(
HttpApiEndpoint.get("health.get", "/api/health", {
success: Schema.Struct({ healthy: Schema.Literal(true) }),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.health.get",
summary: "Check server health",
description: "Check whether the API server is ready to accept requests.",
}),
),
)

View file

@ -1,16 +1,8 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { AgentV2 } from "@opencode-ai/core/agent"
import { CommandV2 } from "@opencode-ai/core/command"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { SkillV2 } from "@opencode-ai/core/skill"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { QuestionV2 } from "@opencode-ai/core/question"
import { Effect, Layer, Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"
@ -22,7 +14,7 @@ export const LocationQuery = Schema.Struct({
workspace: Schema.optional(Schema.String),
}),
),
}).annotate({ identifier: "V2LocationQuery" })
}).annotate({ identifier: "LocationQuery" })
export const locationQueryOpenApi = OpenApi.annotations({
transform: (operation) => {
@ -53,22 +45,14 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
})
}
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
V2LocationMiddleware,
export type LocationServices = Layer.Success<ReturnType<typeof LocationServiceMap.get>>
export class LocationMiddleware extends HttpApiMiddleware.Service<
LocationMiddleware,
{
provides:
| Catalog.Service
| AgentV2.Service
| CommandV2.Service
| Location.Service
| PluginBoot.Service
| PermissionV2.Service
| ProjectReference.Service
| FileSystem.Service
| SkillV2.Service
| QuestionV2.Service
provides: LocationServices
}
>()("@opencode/ExperimentalHttpApiV2Location") {}
>()("@opencode/HttpApiLocation") {}
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
const query = new URL(request.url, "http://localhost").searchParams
@ -82,10 +66,10 @@ function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
}
export const layer = Layer.effect(
V2LocationMiddleware,
LocationMiddleware,
Effect.gen(function* () {
const locations = yield* LocationServiceMap
return V2LocationMiddleware.of((effect) =>
return LocationMiddleware.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))

View file

@ -2,10 +2,10 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
import { SessionLocationMiddleware } from "../middleware/session-location"
export const MessagesQuery = Schema.Struct({
export const SessionMessagesQuery = Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
).annotate({
@ -20,34 +20,35 @@ export const MessagesQuery = Schema.Struct({
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
),
}).annotate({ identifier: "V2SessionMessagesQuery" })
}).annotate({ identifier: "SessionMessagesQuery" })
export const MessageGroup = HttpApiGroup.make("v2.message")
export const MessageGroup = HttpApiGroup.make("server.message")
.add(
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", {
params: { sessionID: SessionV2.ID },
query: MessagesQuery,
query: SessionMessagesQuery,
success: Schema.Struct({
data: Schema.Array(SessionMessage.Message),
cursor: Schema.Struct({
previous: Schema.String.pipe(Schema.optional),
next: Schema.String.pipe(Schema.optional),
}),
}).annotate({ identifier: "V2SessionMessagesResponse" }),
}).annotate({ identifier: "SessionMessagesResponse" }),
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.messages",
summary: "Get v2 session messages",
description:
"Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
}),
),
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.messages",
summary: "Get session messages",
description:
"Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 messages",
description: "Experimental v2 message routes.",
title: "messages",
description: "Experimental message routes.",
}),
)
.middleware(V2Authorization)

View file

@ -2,13 +2,12 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { ServiceUnavailableError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { ServiceUnavailableError } from "../errors"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const ModelGroup = HttpApiGroup.make("v2.model")
export const ModelGroup = HttpApiGroup.make("server.model")
.add(
HttpApiEndpoint.get("models", "/api/model", {
HttpApiEndpoint.get("model.list", "/api/model", {
query: LocationQuery,
success: Location.response(Schema.Array(ModelV2.Info)),
error: ServiceUnavailableError,
@ -17,16 +16,15 @@ export const ModelGroup = HttpApiGroup.make("v2.model")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.model.list",
summary: "List v2 models",
description: "Retrieve available v2 models ordered by release date.",
summary: "List models",
description: "Retrieve available models ordered by release date.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 models",
description: "Experimental v2 model routes.",
title: "models",
description: "Experimental model routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -5,13 +5,13 @@ import { ProjectV2 } from "@opencode-ai/core/project"
import { SessionV2 } from "@opencode-ai/core/session"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { PermissionNotFoundError, SessionNotFoundError } from "../errors"
import { SessionLocationMiddleware } from "../middleware/session-location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const PermissionGroup = HttpApiGroup.make("v2.permission")
export const PermissionGroup = HttpApiGroup.make("server.permission")
.add(
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
HttpApiEndpoint.get("permission.request.list", "/api/permission/request", {
query: LocationQuery,
success: Location.response(Schema.Array(PermissionV2.Request)),
})
@ -24,49 +24,8 @@ export const PermissionGroup = HttpApiGroup.make("v2.permission")
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "v2 permissions", description: "Experimental v2 permission routes." }))
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
.add(
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
params: { sessionID: SessionV2.ID },
success: Schema.Struct({ data: Schema.Array(PermissionV2.Request) }),
error: SessionNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.list",
summary: "List session permission requests",
description: "Retrieve pending permission requests owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("permissionRequestReply", "/api/session/:sessionID/permission/request/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
payload: Schema.Struct({
reply: PermissionV2.Reply,
message: Schema.String.pipe(Schema.optional),
}),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, PermissionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.reply",
summary: "Reply to pending permission request",
description: "Respond to a pending permission request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 session permissions", description: "Experimental v2 session permission routes." }),
)
.middleware(V2Authorization)
export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
.add(
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
HttpApiEndpoint.get("permission.saved.list", "/api/permission/saved", {
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
success: Schema.Struct({ data: Schema.Array(PermissionSaved.Info) }),
}).annotateMerge(
@ -78,7 +37,7 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
),
)
.add(
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
HttpApiEndpoint.delete("permission.saved.remove", "/api/permission/saved/:id", {
params: { id: PermissionSaved.ID },
success: HttpApiSchema.NoContent,
}).annotateMerge(
@ -89,7 +48,41 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }),
.middleware(LocationMiddleware)
.add(
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
params: { sessionID: SessionV2.ID },
success: Schema.Struct({ data: Schema.Array(PermissionV2.Request) }),
error: SessionNotFoundError,
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.list",
summary: "List session permission requests",
description: "Retrieve pending permission requests owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.permission.reply", "/api/session/:sessionID/permission/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
payload: Schema.Struct({
reply: PermissionV2.Reply,
message: Schema.String.pipe(Schema.optional),
}),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, PermissionNotFoundError],
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.permission.reply",
summary: "Reply to pending permission request",
description: "Respond to a pending permission request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." }),
)
.middleware(V2Authorization)

View file

@ -2,13 +2,12 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { ProviderNotFoundError, ServiceUnavailableError } from "../errors"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const ProviderGroup = HttpApiGroup.make("v2.provider")
export const ProviderGroup = HttpApiGroup.make("server.provider")
.add(
HttpApiEndpoint.get("providers", "/api/provider", {
HttpApiEndpoint.get("provider.list", "/api/provider", {
query: LocationQuery,
success: Location.response(Schema.Array(ProviderV2.Info)),
error: ServiceUnavailableError,
@ -17,13 +16,13 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.provider.list",
summary: "List v2 providers",
description: "Retrieve active v2 AI providers so clients can show provider availability and configuration.",
summary: "List providers",
description: "Retrieve active AI providers so clients can show provider availability and configuration.",
}),
),
)
.add(
HttpApiEndpoint.get("provider", "/api/provider/:providerID", {
HttpApiEndpoint.get("provider.get", "/api/provider/:providerID", {
params: { providerID: ProviderV2.ID },
query: LocationQuery,
success: Location.response(ProviderV2.Info),
@ -33,17 +32,16 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.provider.get",
summary: "Get v2 provider",
summary: "Get provider",
description:
"Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.",
"Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 providers",
description: "Experimental v2 provider routes.",
title: "providers",
description: "Experimental provider routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -0,0 +1,60 @@
import { QuestionV2 } from "@opencode-ai/core/question"
import { Location } from "@opencode-ai/core/location"
import { SessionV2 } from "@opencode-ai/core/session"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { QuestionNotFoundError, SessionNotFoundError } from "../errors"
import { SessionLocationMiddleware } from "../middleware/session-location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const QuestionGroup = HttpApiGroup.make("server.question")
.add(
HttpApiEndpoint.get("question.request.list", "/api/question/request", {
query: LocationQuery,
success: Location.response(Schema.Array(QuestionV2.Request)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.question.request.list",
summary: "List pending question requests",
description: "Retrieve pending question requests for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "questions", description: "Experimental question routes." }))
.middleware(LocationMiddleware)
.add(
HttpApiEndpoint.post("session.question.reply", "/api/session/:sessionID/question/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
payload: QuestionV2.Reply,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reply",
summary: "Reply to pending question request",
description: "Answer a pending question request owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.question.reject", "/api/session/:sessionID/question/:requestID/reject", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reject",
summary: "Reject pending question request",
description: "Reject a pending question request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "session questions", description: "Experimental session question routes." }),
)

View file

@ -14,8 +14,8 @@ import {
ServiceUnavailableError,
SessionNotFoundError,
UnknownError,
} from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
} from "../errors"
import { SessionLocationMiddleware } from "../middleware/session-location"
const SessionsQueryFields = {
workspace: WorkspaceV2.ID.pipe(Schema.optional),
@ -57,7 +57,7 @@ const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
export const SessionsCursor = Schema.String.pipe(
Schema.brand("V2SessionsCursor"),
Schema.brand("SessionsCursor"),
withStatics((schema) => {
const make = schema.make
return {
@ -82,11 +82,11 @@ export const SessionsQuery = Schema.Struct({
project: ProjectV2.ID.pipe(Schema.optional),
subpath: RelativePath.pipe(Schema.optional),
cursor: SessionsCursorQuery.fields.cursor.pipe(Schema.optional),
}).annotate({ identifier: "V2SessionsQuery" })
}).annotate({ identifier: "SessionsQuery" })
export const SessionGroup = HttpApiGroup.make("v2.session")
export const SessionGroup = HttpApiGroup.make("server.session")
.add(
HttpApiEndpoint.get("sessions", "/api/session", {
HttpApiEndpoint.get("session.list", "/api/session", {
query: SessionsQuery,
success: Schema.Struct({
data: Schema.Array(SessionV2.Info),
@ -94,19 +94,19 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
previous: SessionsCursor.pipe(Schema.optional),
next: SessionsCursor.pipe(Schema.optional),
}),
}).annotate({ identifier: "V2SessionsResponse" }),
}).annotate({ identifier: "SessionsResponse" }),
error: [InvalidCursorError, InvalidRequestError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.list",
summary: "List v2 sessions",
summary: "List sessions",
description:
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
}),
),
)
.add(
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
params: { sessionID: SessionV2.ID },
payload: Schema.Struct({
id: SessionMessage.ID.pipe(Schema.optional),
@ -116,57 +116,64 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
}),
success: Schema.Struct({ data: SessionInput.Admitted }),
error: [ConflictError, SessionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.prompt",
summary: "Send v2 message",
description: "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.",
}),
),
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.prompt",
summary: "Send message",
description: "Durably admit one session input and schedule agent-loop execution unless resume is false.",
}),
),
)
.add(
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
HttpApiEndpoint.post("session.compact", "/api/session/:sessionID/compact", {
params: { sessionID: SessionV2.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, ServiceUnavailableError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.compact",
summary: "Compact v2 session",
description: "Compact a v2 session conversation.",
}),
),
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.compact",
summary: "Compact session",
description: "Compact a session conversation.",
}),
),
)
.add(
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
HttpApiEndpoint.post("session.wait", "/api/session/:sessionID/wait", {
params: { sessionID: SessionV2.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, ServiceUnavailableError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.wait",
summary: "Wait for v2 session",
description: "Wait for a v2 session agent loop to become idle.",
}),
),
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.wait",
summary: "Wait for session",
description: "Wait for a session agent loop to become idle.",
}),
),
)
.add(
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", {
params: { sessionID: SessionV2.ID },
success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }),
error: [SessionNotFoundError, UnknownError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.context",
summary: "Get v2 session context",
description: "Retrieve the active context messages for a v2 session (all messages after the last compaction).",
}),
),
})
.middleware(SessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.context",
summary: "Get session context",
description: "Retrieve the active context messages for a session (all messages after the last compaction).",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2",
description: "Experimental v2 routes.",
title: "sessions",
description: "Experimental session routes.",
}),
)
.middleware(V2Authorization)

View file

@ -2,12 +2,11 @@ import { SkillV2 } from "@opencode-ai/core/skill"
import { Location } from "@opencode-ai/core/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
export const SkillGroup = HttpApiGroup.make("v2.skill")
export const SkillGroup = HttpApiGroup.make("server.skill")
.add(
HttpApiEndpoint.get("skills", "/api/skill", {
HttpApiEndpoint.get("skill.list", "/api/skill", {
query: LocationQuery,
success: Location.response(Schema.Array(SkillV2.Info)),
})
@ -15,16 +14,15 @@ export const SkillGroup = HttpApiGroup.make("v2.skill")
.annotateMerge(
OpenApi.annotations({
identifier: "v2.skill.list",
summary: "List v2 skills",
description: "Retrieve currently registered v2 skills.",
summary: "List skills",
description: "Retrieve currently registered skills.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 skills",
description: "Experimental v2 skill routes.",
title: "skills",
description: "Experimental skill routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
.middleware(LocationMiddleware)

View file

@ -1,17 +0,0 @@
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
export const HealthGroup = HttpApiGroup.make("v2.health")
.add(
HttpApiEndpoint.get("health", "/api/health", {
success: Schema.Struct({ healthy: Schema.Literal(true) }),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.health.get",
summary: "Check v2 server health",
description: "Check whether the v2 API server is ready to accept requests.",
}),
),
)
.middleware(V2Authorization)

View file

@ -1,60 +0,0 @@
import { QuestionV2 } from "@opencode-ai/core/question"
import { Location } from "@opencode-ai/core/location"
import { SessionV2 } from "@opencode-ai/core/session"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
export const QuestionGroup = HttpApiGroup.make("v2.question")
.add(
HttpApiEndpoint.get("questionRequests", "/api/question/request", {
query: LocationQuery,
success: Location.response(Schema.Array(QuestionV2.Request)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.question.request.list",
summary: "List pending question requests",
description: "Retrieve pending question requests for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "v2 questions", description: "Experimental v2 question routes." }))
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)
export const SessionQuestionGroup = HttpApiGroup.make("v2.session.question")
.add(
HttpApiEndpoint.post("questionRequestReply", "/api/session/:sessionID/question/request/:requestID/reply", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
payload: QuestionV2.Reply,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reply",
summary: "Reply to pending question request",
description: "Answer a pending question request owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("questionRequestReject", "/api/session/:sessionID/question/request/:requestID/reject", {
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reject",
summary: "Reject pending question request",
description: "Reject a pending question request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "v2 session questions", description: "Experimental v2 session question routes." }),
)
.middleware(V2Authorization)