| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { SessionV2 } from "@kirincode-ai/core/session"
- import { DateTime, Effect, Stream } from "effect"
- import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
- import { Api } from "../api"
- import { SessionsCursor } from "@kirincode-ai/protocol/groups/session"
- import {
- ConflictError,
- InvalidCursorError,
- MessageNotFoundError,
- ServiceUnavailableError,
- SessionNotFoundError,
- UnknownError,
- } from "@kirincode-ai/protocol/errors"
- import { AbsolutePath } from "@kirincode-ai/core/schema"
- const DefaultSessionsLimit = 50
- const DefaultSessionHistoryLimit = 50
- export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- return handlers
- .handle(
- "session.list",
- Effect.fn(function* (ctx) {
- const query =
- ctx.query.cursor !== undefined
- ? yield* SessionsCursor.parse(ctx.query.cursor).pipe(
- Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" })),
- )
- : ctx.query
- const sessions = yield* session.list({
- ...query,
- workspaceID: query.workspace,
- limit: ctx.query.limit ?? DefaultSessionsLimit,
- })
- const first = sessions[0]
- const last = sessions.at(-1)
- return {
- data: sessions,
- cursor: {
- previous: first
- ? SessionsCursor.make({
- ...query,
- anchor: {
- id: first.id,
- time: DateTime.toEpochMillis(first.time.created),
- direction: "previous",
- },
- })
- : undefined,
- next: last
- ? SessionsCursor.make({
- ...query,
- anchor: {
- id: last.id,
- time: DateTime.toEpochMillis(last.time.created),
- direction: "next",
- },
- })
- : undefined,
- },
- }
- }),
- )
- .handle(
- "session.create",
- Effect.fn(function* (ctx) {
- return {
- data: yield* session.create({
- id: ctx.payload.id,
- agent: ctx.payload.agent,
- model: ctx.payload.model,
- location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) },
- }),
- }
- }),
- )
- .handle(
- "session.active",
- Effect.fn(function* () {
- return {
- data: Object.fromEntries(
- Array.from(yield* session.active, (sessionID) => [sessionID, { type: "running" as const }]),
- ),
- }
- }),
- )
- .handle(
- "session.get",
- Effect.fn(function* (ctx) {
- return {
- data: yield* session.get(ctx.params.sessionID).pipe(
- Effect.catchTag(
- "Session.NotFoundError",
- (error) =>
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- ),
- }
- }),
- )
- .handle(
- "session.switchAgent",
- Effect.fn(function* (ctx) {
- yield* session.switchAgent({ sessionID: ctx.params.sessionID, agent: ctx.payload.agent }).pipe(
- Effect.catchTag("Session.NotFoundError", (error) =>
- Effect.fail(
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- ),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.switchModel",
- Effect.fn(function* (ctx) {
- yield* session.switchModel({ sessionID: ctx.params.sessionID, model: ctx.payload.model }).pipe(
- Effect.catchTag("Session.NotFoundError", (error) =>
- Effect.fail(
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- ),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.prompt",
- Effect.fn(function* (ctx) {
- 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,
- }),
- ),
- ),
- ),
- }
- }),
- )
- .handle(
- "session.compact",
- Effect.fn(function* (ctx) {
- yield* session.compact({ sessionID: 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.OperationUnavailableError", (error) =>
- Effect.fail(
- new ServiceUnavailableError({
- message: `Session ${error.operation} is not available yet`,
- service: `session.${error.operation}`,
- }),
- ),
- ),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.wait",
- Effect.fn(function* (ctx) {
- yield* session.wait(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.OperationUnavailableError", (error) =>
- Effect.fail(
- new ServiceUnavailableError({
- message: `Session ${error.operation} is not available yet`,
- service: `session.${error.operation}`,
- }),
- ),
- ),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.revert.stage",
- Effect.fn(function* (ctx) {
- return {
- data: yield* session.revert.stage({ ...ctx.params, ...ctx.payload }).pipe(
- Effect.catchTag(
- "Session.NotFoundError",
- (error) =>
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- Effect.catchTag(
- "Session.MessageNotFoundError",
- (error) =>
- new MessageNotFoundError({
- sessionID: error.sessionID,
- messageID: error.messageID,
- message: `Message not found: ${error.messageID}`,
- }),
- ),
- Effect.catchTag("Snapshot.Error", (error) => {
- const ref = `err_${crypto.randomUUID().slice(0, 8)}`
- return Effect.logError("failed to stage session revert", { cause: error }).pipe(
- Effect.andThen(
- Effect.fail(
- new UnknownError({
- message: "Unexpected server error. Check server logs for details.",
- ref,
- }),
- ),
- ),
- )
- }),
- ),
- }
- }),
- )
- .handle(
- "session.revert.clear",
- Effect.fn(function* (ctx) {
- yield* session.revert.clear(ctx.params.sessionID).pipe(
- Effect.catchTag(
- "Session.NotFoundError",
- (error) =>
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- Effect.catchTag("Snapshot.Error", (error) => {
- const ref = `err_${crypto.randomUUID().slice(0, 8)}`
- return Effect.logError("failed to clear session revert", { cause: error }).pipe(
- Effect.andThen(
- Effect.fail(
- new UnknownError({
- message: "Unexpected server error. Check server logs for details.",
- ref,
- }),
- ),
- ),
- )
- }),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.revert.commit",
- Effect.fn(function* (ctx) {
- yield* session.revert.commit(ctx.params.sessionID).pipe(
- Effect.catchTag(
- "Session.NotFoundError",
- (error) =>
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- )
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.context",
- Effect.fn(function* (ctx) {
- 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 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 }),
- ),
- ),
- )
- }),
- ),
- }
- }),
- )
- .handle(
- "session.history",
- Effect.fn(function* (ctx) {
- return yield* session
- .history({
- sessionID: ctx.params.sessionID,
- after: ctx.query.after,
- limit: ctx.query.limit ?? DefaultSessionHistoryLimit,
- })
- .pipe(
- Effect.map((page) => ({
- data: page.events,
- hasMore: page.hasMore,
- })),
- Effect.catchTag(
- "Session.NotFoundError",
- (error) =>
- new SessionNotFoundError({
- sessionID: error.sessionID,
- message: `Session not found: ${error.sessionID}`,
- }),
- ),
- )
- }),
- )
- .handle(
- "session.events",
- Effect.fn((ctx) =>
- Effect.succeed(
- session.events({ sessionID: ctx.params.sessionID, after: ctx.query.after }).pipe(Stream.orDie),
- ),
- ),
- )
- .handle(
- "session.interrupt",
- Effect.fn(function* (ctx) {
- yield* session.interrupt(ctx.params.sessionID)
- return HttpApiSchema.NoContent.make()
- }),
- )
- .handle(
- "session.message",
- Effect.fn(function* (ctx) {
- const message = yield* session.message(ctx.params)
- if (message) return { data: message }
- return yield* new MessageNotFoundError({
- sessionID: ctx.params.sessionID,
- messageID: ctx.params.messageID,
- message: `Message not found: ${ctx.params.messageID}`,
- })
- }),
- )
- }),
- )
|