chore: merge dev
This commit is contained in:
commit
d3fe65e2f7
312 changed files with 32857 additions and 4562 deletions
|
|
@ -11,6 +11,7 @@ import { SkillGroup } from "./groups/v2/skill"
|
|||
import { EventGroup } from "./groups/v2/event"
|
||||
import { AgentGroup } from "./groups/v2/agent"
|
||||
import { HealthGroup } from "./groups/v2/health"
|
||||
import { QuestionGroup, SessionQuestionGroup } from "./groups/v2/question"
|
||||
|
||||
export const V2Api = HttpApi.make("v2")
|
||||
.add(HealthGroup)
|
||||
|
|
@ -26,6 +27,8 @@ export const V2Api = HttpApi.make("v2")
|
|||
.add(CommandGroup)
|
||||
.add(SkillGroup)
|
||||
.add(EventGroup)
|
||||
.add(QuestionGroup)
|
||||
.add(SessionQuestionGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ export class UnauthorizedError extends Schema.TaggedErrorClass<UnauthorizedError
|
|||
{ httpApiStatus: 401 },
|
||||
) {}
|
||||
|
||||
export class ConflictError extends Schema.TaggedErrorClass<ConflictError>()(
|
||||
"ConflictError",
|
||||
{
|
||||
message: Schema.String,
|
||||
resource: Schema.optional(Schema.String),
|
||||
},
|
||||
{ httpApiStatus: 409 },
|
||||
) {}
|
||||
|
||||
export class ServiceUnavailableError extends Schema.TaggedErrorClass<ServiceUnavailableError>()(
|
||||
"ServiceUnavailableError",
|
||||
{
|
||||
|
|
@ -66,3 +75,12 @@ export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionN
|
|||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
|
||||
"QuestionNotFoundError",
|
||||
{
|
||||
requestID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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"
|
||||
|
|
@ -64,16 +66,18 @@ export class V2LocationMiddleware extends HttpApiMiddleware.Service<
|
|||
| ProjectReference.Service
|
||||
| FileSystem.Service
|
||||
| SkillV2.Service
|
||||
| QuestionV2.Service
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiV2Location") {}
|
||||
|
||||
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
const query = new URL(request.url, "http://localhost").searchParams
|
||||
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
||||
return {
|
||||
directory: AbsolutePath.make(
|
||||
query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
|
||||
),
|
||||
workspaceID: query.get("location[workspace]") || request.headers["x-opencode-workspace"],
|
||||
workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { Schema } from "effect"
|
|||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { data } from "./response"
|
||||
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
|
|
@ -28,13 +27,13 @@ export const MessageGroup = HttpApiGroup.make("v2.message")
|
|||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
query: MessagesQuery,
|
||||
success: data(Schema.Struct({
|
||||
items: Schema.Array(SessionMessage.Message),
|
||||
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: "V2SessionMessagesResponse" }),
|
||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/un
|
|||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
import { data } from "./response"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
.add(
|
||||
|
|
@ -33,7 +32,7 @@ export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
|
|||
.add(
|
||||
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: data(Schema.Array(PermissionV2.Request)),
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionV2.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
@ -69,7 +68,7 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
|||
.add(
|
||||
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: data(Schema.Array(PermissionSaved.Info)),
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionSaved.Info) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
|
|
|
|||
60
packages/server/src/groups/v2/question.ts
Normal file
60
packages/server/src/groups/v2/question.ts
Normal 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 { 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)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { Schema } from "effect"
|
||||
|
||||
export function data<S extends Schema.Top>(schema: S) {
|
||||
return Schema.Struct({ data: schema })
|
||||
}
|
||||
|
||||
export function make<A>(data: A) {
|
||||
return { data }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
|
|
@ -7,6 +8,7 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
|||
import { Schema, Struct } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import {
|
||||
ConflictError,
|
||||
InvalidCursorError,
|
||||
InvalidRequestError,
|
||||
ServiceUnavailableError,
|
||||
|
|
@ -14,7 +16,6 @@ import {
|
|||
UnknownError,
|
||||
} from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { data } from "./response"
|
||||
|
||||
const SessionsQueryFields = {
|
||||
workspace: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
|
|
@ -87,13 +88,13 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
query: SessionsQuery,
|
||||
success: data(Schema.Struct({
|
||||
items: Schema.Array(SessionV2.Info),
|
||||
success: Schema.Struct({
|
||||
data: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
previous: SessionsCursor.pipe(Schema.optional),
|
||||
next: SessionsCursor.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" })),
|
||||
}).annotate({ identifier: "V2SessionsResponse" }),
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
@ -108,16 +109,18 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
prompt: Prompt,
|
||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: data(SessionMessage.Message),
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
success: Schema.Struct({ data: SessionMessage.User }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.prompt",
|
||||
summary: "Send v2 message",
|
||||
description: "Create a v2 session message and queue it for the agent loop.",
|
||||
description: "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -150,7 +153,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
.add(
|
||||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: data(Schema.Array(SessionMessage.Message)),
|
||||
success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }),
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
|
|||
|
|
@ -14,6 +14,24 @@ import { skillHandlers } from "./handlers/v2/skill"
|
|||
import { eventHandlers } from "./handlers/v2/event"
|
||||
import { agentHandlers } from "./handlers/v2/agent"
|
||||
import { healthHandlers } from "./handlers/v2/health"
|
||||
import { questionHandlers, sessionQuestionHandlers } from "./handlers/v2/question"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
|
||||
const routedSessions = SessionV2.layer.pipe(
|
||||
Layer.provide(SessionProjector.layer),
|
||||
Layer.provide(SessionExecutionLocal.layer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(SessionStore.layer),
|
||||
Layer.provide(EventV2.layer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(ProjectV2.defaultLayer),
|
||||
Layer.orDie,
|
||||
)
|
||||
|
||||
export const v2Handlers = Layer.mergeAll(
|
||||
healthHandlers,
|
||||
|
|
@ -29,9 +47,11 @@ export const v2Handlers = Layer.mergeAll(
|
|||
commandHandlers,
|
||||
skillHandlers,
|
||||
eventHandlers,
|
||||
questionHandlers,
|
||||
sessionQuestionHandlers,
|
||||
).pipe(
|
||||
Layer.provide(v2LocationLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(PermissionSaved.layer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
Layer.provide(routedSessions),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { make } from "../../groups/v2/response"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
|
||||
const Cursor = Schema.Struct({
|
||||
id: SessionMessage.ID,
|
||||
time: Schema.Finite,
|
||||
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
})
|
||||
|
|
@ -20,9 +17,7 @@ const decodeCursor = Schema.decodeUnknownSync(Cursor)
|
|||
|
||||
const cursor = {
|
||||
encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }),
|
||||
).toString("base64url")
|
||||
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
||||
|
|
@ -48,7 +43,7 @@ export const messageHandlers = HttpApiBuilder.group(V2Api, "v2.message", (handle
|
|||
sessionID: ctx.params.sessionID,
|
||||
limit: ctx.query.limit ?? DefaultMessagesLimit,
|
||||
order,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
|
|
@ -76,13 +71,13 @@ export const messageHandlers = HttpApiBuilder.group(V2Api, "v2.message", (handle
|
|||
)
|
||||
const first = messages[0]
|
||||
const last = messages.at(-1)
|
||||
return make({
|
||||
items: messages,
|
||||
return {
|
||||
data: messages,
|
||||
cursor: {
|
||||
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
||||
next: last ? cursor.encode(last, order, "next") : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|||
import { V2Api } from "../../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
import { make } from "../../groups/v2/response"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
|
|
@ -63,7 +62,7 @@ export const sessionPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.session
|
|||
"sessionPermissionRequests",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
permission.forSession(ctx.params.sessionID).pipe(Effect.map(make)),
|
||||
permission.forSession(ctx.params.sessionID).pipe(Effect.map((data) => ({ data }))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -93,7 +92,7 @@ export const savedPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.permissio
|
|||
.handle(
|
||||
"savedPermissions",
|
||||
Effect.fn(function* (ctx) {
|
||||
return make(yield* saved.list({ projectID: ctx.query.projectID }))
|
||||
return { data: yield* saved.list({ projectID: ctx.query.projectID }) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
|
|
|
|||
97
packages/server/src/handlers/v2/question.ts
Normal file
97
packages/server/src/handlers/v2/question.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
function missingRequest(id: QuestionV2.ID) {
|
||||
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const questionHandlers = HttpApiBuilder.group(V2Api, "v2.question", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"questionRequests",
|
||||
Effect.fn(function* () {
|
||||
return yield* response((yield* QuestionV2.Service).list())
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const sessionQuestionHandlers = HttpApiBuilder.group(V2Api, "v2.session.question", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const withSessionQuestion = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: QuestionV2.Request["sessionID"],
|
||||
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* use(yield* QuestionV2.Service)
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const withOwnedQuestion = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: QuestionV2.Request["sessionID"],
|
||||
requestID: QuestionV2.ID,
|
||||
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
return yield* withSessionQuestion(sessionID, (question) =>
|
||||
Effect.gen(function* () {
|
||||
const request = (yield* question.list()).find((request) => request.id === requestID)
|
||||
if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID)
|
||||
return yield* use(question)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"questionRequestReply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers })
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"questionRequestReject",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reject(ctx.params.requestID)
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -3,8 +3,13 @@ import { DateTime, Effect } from "effect"
|
|||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { SessionsCursor } from "../../groups/v2/session"
|
||||
import { InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { make } from "../../groups/v2/response"
|
||||
import {
|
||||
ConflictError,
|
||||
InvalidCursorError,
|
||||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../../errors"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
|
||||
|
|
@ -29,8 +34,8 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
})
|
||||
const first = sessions[0]
|
||||
const last = sessions.at(-1)
|
||||
return make({
|
||||
items: sessions,
|
||||
return {
|
||||
data: sessions,
|
||||
cursor: {
|
||||
previous: first
|
||||
? SessionsCursor.make({
|
||||
|
|
@ -53,36 +58,40 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
})
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
return make(yield* session
|
||||
.prompt({
|
||||
sessionID: ctx.params.sessionID,
|
||||
prompt: ctx.payload.prompt,
|
||||
delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
return {
|
||||
data: yield* session
|
||||
.prompt({
|
||||
sessionID: ctx.params.sessionID,
|
||||
id: ctx.payload.id,
|
||||
prompt: ctx.payload.prompt,
|
||||
delivery: ctx.payload.delivery,
|
||||
resume: ctx.payload.resume,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.PromptConflictError", (error) =>
|
||||
Effect.fail(
|
||||
new ConflictError({
|
||||
message: `Prompt message ID conflicts with an existing durable record: ${error.messageID}`,
|
||||
resource: error.messageID,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.OperationUnavailableError", (error) =>
|
||||
Effect.fail(
|
||||
new ServiceUnavailableError({
|
||||
message: `V2 session ${error.operation} is not available yet`,
|
||||
service: `v2.session.${error.operation}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
@ -136,30 +145,32 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
.handle(
|
||||
"context",
|
||||
Effect.fn(function* (ctx) {
|
||||
return make(yield* session.context(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to decode v2 session message").pipe(
|
||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
||||
Effect.andThen(
|
||||
Effect.fail(
|
||||
new UnknownError({
|
||||
message: "Unexpected server error. Check server logs for details.",
|
||||
ref,
|
||||
}),
|
||||
),
|
||||
return {
|
||||
data: yield* session.context(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
))
|
||||
),
|
||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to decode v2 session message").pipe(
|
||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
||||
Effect.andThen(
|
||||
Effect.fail(
|
||||
new UnknownError({
|
||||
message: "Unexpected server error. Check server logs for details.",
|
||||
ref,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue