diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 40f209227..f70fafaf7 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,9 +1,8 @@ export * as SessionV2 from "./session" +export * from "./session/schema" import { DateTime, Effect, Layer, Schema, Context } from "effect" import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "drizzle-orm" -import { AbsolutePath, RelativePath, withStatics } from "./schema" -import { Identifier } from "./util/identifier" import { Project } from "./project" import { WorkspaceV2 } from "./workspace" import { ModelV2 } from "./model" @@ -11,60 +10,10 @@ import { Location } from "./location" import { SessionMessage } from "./session/message" import type { Prompt } from "./session/prompt" import { EventV2 } from "./event" -import { optionalOmitUndefined } from "./schema" -import { V2Schema } from "./v2-schema" import { ProviderV2 } from "./provider" import { Database } from "./database/database" import { SessionMessageTable, SessionTable } from "./session/sql" - -export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({ - identifier: "Session.Delivery", -}) -export type Delivery = Schema.Schema.Type - -export const DefaultDelivery = "immediate" satisfies Delivery - -export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( - Schema.brand("SessionID"), - withStatics((schema) => ({ - descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()), - })), -) -export type ID = typeof ID.Type - -export const LegacyInfo = Schema.Struct({ - id: ID, - location: Location.Ref, - subpath: RelativePath, // derived from location - project: Project.ID, // derived from location -}) -export type LegacyInfo = typeof LegacyInfo.Type - -export class Info extends Schema.Class("Session.Info")({ - id: ID, - parentID: optionalOmitUndefined(ID), - projectID: Project.ID, - workspaceID: optionalOmitUndefined(WorkspaceV2.ID), - path: optionalOmitUndefined(Schema.String), - agent: optionalOmitUndefined(Schema.String), - model: ModelV2.Ref.pipe(optionalOmitUndefined), - cost: Schema.Finite, - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - updated: V2Schema.DateTimeUtcFromMillis, - archived: optionalOmitUndefined(V2Schema.DateTimeUtcFromMillis), - }), - title: Schema.String, -}) {} +import { SessionSchema } from "./session/schema" // get project -> project.locations // @@ -76,7 +25,7 @@ export class Info extends Schema.Class("Session.Info")({ // - by workspace (home is special) type Cursor = { - id: ID + id: SessionSchema.ID time: number direction: "previous" | "next" } @@ -95,26 +44,26 @@ type ListInput = { } type CreateInput = { - id?: ID + id?: SessionSchema.ID agent?: string model?: ModelV2.Ref location?: Location.Ref - parentID?: ID + parentID?: SessionSchema.ID workspaceID?: WorkspaceV2.ID } type MoveInput = { - sessionID: ID + sessionID: SessionSchema.ID location: Location.Ref } type CompactInput = { - sessionID: ID + sessionID: SessionSchema.ID prompt?: Prompt } export class NotFoundError extends Schema.TaggedErrorClass()("Session.NotFoundError", { - sessionID: ID, + sessionID: SessionSchema.ID, }) {} export class OperationUnavailableError extends Schema.TaggedErrorClass()( @@ -125,19 +74,19 @@ export class OperationUnavailableError extends Schema.TaggedErrorClass()("Session.MessageDecodeError", { - sessionID: ID, + sessionID: SessionSchema.ID, messageID: SessionMessage.ID, }) {} export type Error = NotFoundError | OperationUnavailableError | MessageDecodeError export interface Interface { - readonly list: (input?: ListInput) => Effect.Effect - readonly create: (input?: CreateInput) => Effect.Effect + readonly list: (input?: ListInput) => Effect.Effect + readonly create: (input?: CreateInput) => Effect.Effect readonly move: (input: MoveInput) => Effect.Effect - readonly get: (sessionID: ID) => Effect.Effect + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect readonly messages: (input: { - sessionID: ID + sessionID: SessionSchema.ID limit?: number order?: "asc" | "desc" cursor?: { @@ -146,52 +95,52 @@ export interface Interface { direction: "previous" | "next" } }) => Effect.Effect - readonly context: (sessionID: ID) => Effect.Effect + readonly context: (sessionID: SessionSchema.ID) => Effect.Effect readonly subagent: (input: { id?: EventV2.ID - parentID: ID + parentID: SessionSchema.ID prompt: Prompt agent: string model?: ModelV2.Ref resume?: boolean }) => Effect.Effect - readonly switchAgent: (input: { sessionID: ID; agent: string }) => Effect.Effect - readonly switchModel: (input: { sessionID: ID; model: ModelV2.Ref }) => Effect.Effect + readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchModel: (input: { sessionID: SessionSchema.ID; model: ModelV2.Ref }) => Effect.Effect readonly prompt: (input: { id?: EventV2.ID - sessionID: ID + sessionID: SessionSchema.ID prompt: Prompt - delivery?: Delivery + delivery?: SessionSchema.Delivery resume?: boolean }) => Effect.Effect readonly shell: (input: { id?: EventV2.ID - sessionID: ID + sessionID: SessionSchema.ID command: string - delivery?: Delivery + delivery?: SessionSchema.Delivery resume?: boolean }) => Effect.Effect readonly skill: (input: { id?: EventV2.ID - sessionID: ID + sessionID: SessionSchema.ID skill: string - delivery?: Delivery + delivery?: SessionSchema.Delivery resume?: boolean }) => Effect.Effect - readonly compact: (input: CompactInput | ID) => Effect.Effect - readonly wait: (id: ID) => Effect.Effect - readonly resume: (sessionID: ID) => Effect.Effect + readonly compact: (input: CompactInput | SessionSchema.ID) => Effect.Effect + readonly wait: (id: SessionSchema.ID) => Effect.Effect + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Session") {} -function fromRow(row: typeof SessionTable.$inferSelect): Info { - return new Info({ - id: ID.make(row.id), +function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { + return new SessionSchema.Info({ + id: SessionSchema.ID.make(row.id), projectID: Project.ID.make(row.project_id), workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, title: row.title, - parentID: row.parent_id ? ID.make(row.parent_id) : undefined, + parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined, path: row.path ?? "", agent: row.agent ?? undefined, model: row.model @@ -230,7 +179,7 @@ export const layer = Layer.effect( Effect.mapError( () => new MessageDecodeError({ - sessionID: ID.make(row.session_id), + sessionID: SessionSchema.ID.make(row.session_id), messageID: SessionMessage.ID.make(row.id), }), ), @@ -238,7 +187,7 @@ export const layer = Layer.effect( const result = Service.of({ create: Effect.fn("V2Session.create")(function* () { - return {} as Info + return {} as SessionSchema.Info }), get: Effect.fn("V2Session.get")(function* (sessionID) { const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie) diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index f774286fd..27119fcf9 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -4,8 +4,8 @@ import { ModelV2 } from "../model" import { NonNegativeInt } from "../schema" import { ToolOutput } from "../tool-output" import { V2Schema } from "../v2-schema" -import { SessionV2 } from "../session" import { FileAttachment, Prompt } from "./prompt" +import { SessionSchema } from "./schema" export { FileAttachment } @@ -20,7 +20,7 @@ export type Source = typeof Source.Type const Base = { timestamp: V2Schema.DateTimeUtcFromMillis, - sessionID: SessionV2.ID, + sessionID: SessionSchema.ID, } const options = { diff --git a/packages/core/src/session/schema.ts b/packages/core/src/session/schema.ts new file mode 100644 index 000000000..fe4626d4a --- /dev/null +++ b/packages/core/src/session/schema.ts @@ -0,0 +1,59 @@ +export * as SessionSchema from "./schema" + +import { Schema } from "effect" +import { Location } from "../location" +import { ModelV2 } from "../model" +import { Project } from "../project" +import { RelativePath, optionalOmitUndefined, withStatics } from "../schema" +import { WorkspaceV2 } from "../workspace" +import { Identifier } from "../util/identifier" +import { V2Schema } from "../v2-schema" + +export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({ + identifier: "Session.Delivery", +}) +export type Delivery = Schema.Schema.Type + +export const DefaultDelivery = "immediate" satisfies Delivery + +export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( + Schema.brand("SessionID"), + withStatics((schema) => ({ + descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()), + })), +) +export type ID = typeof ID.Type + +export const LegacyInfo = Schema.Struct({ + id: ID, + location: Location.Ref, + subpath: RelativePath, // derived from location + project: Project.ID, // derived from location +}) +export type LegacyInfo = typeof LegacyInfo.Type + +export class Info extends Schema.Class("Session.Info")({ + id: ID, + parentID: optionalOmitUndefined(ID), + projectID: Project.ID, + workspaceID: optionalOmitUndefined(WorkspaceV2.ID), + path: optionalOmitUndefined(Schema.String), + agent: optionalOmitUndefined(Schema.String), + model: ModelV2.Ref.pipe(optionalOmitUndefined), + cost: Schema.Finite, + tokens: Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), + }), + time: Schema.Struct({ + created: V2Schema.DateTimeUtcFromMillis, + updated: V2Schema.DateTimeUtcFromMillis, + archived: optionalOmitUndefined(V2Schema.DateTimeUtcFromMillis), + }), + title: Schema.String, +}) {} diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 92bc68dbf..0cea7b913 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -4,7 +4,7 @@ import type { SessionMessage } from "./message" import type { Snapshot } from "../snapshot" import { PermissionV2 } from "../permission" import { Project } from "../project" -import type { ID } from "../session" +import type { SessionSchema } from "./schema" import type { MessageID, PartID } from "./legacy" import { WorkspaceV2 } from "../workspace" import { Timestamps } from "../database/schema.sql" @@ -14,13 +14,13 @@ type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type export const SessionTable = sqliteTable( "session", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), project_id: text() .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), workspace_id: text().$type(), - parent_id: text().$type(), + parent_id: text().$type(), slug: text().notNull(), directory: text().notNull(), path: text(), @@ -61,7 +61,7 @@ export const MessageTable = sqliteTable( { id: text().$type().primaryKey(), session_id: text() - .$type() + .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), ...Timestamps, @@ -78,7 +78,7 @@ export const PartTable = sqliteTable( .$type() .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), - session_id: text().$type().notNull(), + session_id: text().$type().notNull(), ...Timestamps, data: text({ mode: "json" }).notNull(), }, @@ -92,7 +92,7 @@ export const TodoTable = sqliteTable( "todo", { session_id: text() - .$type() + .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), content: text().notNull(), @@ -112,7 +112,7 @@ export const SessionMessageTable = sqliteTable( { id: text().$type().primaryKey(), session_id: text() - .$type() + .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), type: text().$type().notNull(),