Dax 4 mēneši atpakaļ
vecāks
revīzija
7a6ce05d09

+ 1 - 0
packages/opencode/src/id/id.ts

@@ -13,6 +13,7 @@ export namespace Identifier {
     pty: "pty",
     tool: "tool",
     workspace: "wrk",
+    entry: "ent",
   } as const
 
   export function schema(prefix: keyof typeof prefixes) {

+ 31 - 1
packages/opencode/src/session/projectors.ts

@@ -1,10 +1,11 @@
-import { NotFoundError, eq, and } from "../storage/db"
+import { NotFoundError, eq, and, sql } from "../storage/db"
 import { SyncEvent } from "@/sync"
 import { Session } from "./index"
 import { MessageV2 } from "./message-v2"
 import { SessionTable, MessageTable, PartTable } from "./session.sql"
 import { ProjectTable } from "../project/project.sql"
 import { Log } from "../util/log"
+import { DateTime } from "effect"
 
 const log = Log.create({ service: "session.projector" })
 
@@ -132,4 +133,33 @@ export default [
       log.warn("ignored late part update", { partID: id, messageID, sessionID })
     }
   }),
+
+  // Experimental
+  SyncEvent.project(MessageV2.Event.PartUpdated, (db, data) => {
+    /*
+    const id = SessionEntry.ID.make(data.part.id.replace("prt", "ent"))
+    switch (data.part.type) {
+      case "text":
+        db.insert(SessionEntryTable)
+          .values({
+            id,
+            session_id: data.sessionID,
+            type: "text",
+            data: new SessionEntry.Text({
+              id,
+              text: data.part.text,
+              type: "text",
+              time: {
+                created: DateTime.makeUnsafe(data.part.time?.start ?? Date.now()),
+                completed: data.part.time?.end ? DateTime.makeUnsafe(data.part.time.end) : undefined,
+              },
+            }),
+            time_created: Date.now(),
+            time_updated: Date.now(),
+          })
+          .onConflictDoUpdate({ target: SessionEntryTable.id, set: { data: sql`excluded.data` } })
+          .run()
+    }
+    */
+  }),
 ]

+ 23 - 0
packages/opencode/src/session/session.sql.ts

@@ -1,6 +1,7 @@
 import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
 import { ProjectTable } from "../project/project.sql"
 import type { MessageV2 } from "./message-v2"
+import type { SessionEntry } from "../v2/session-entry"
 import type { Snapshot } from "../snapshot"
 import type { Permission } from "../permission"
 import type { ProjectID } from "../project/schema"
@@ -10,6 +11,7 @@ import { Timestamps } from "../storage/schema.sql"
 
 type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
 type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
+type EntryData = Omit<SessionEntry.Entry, "id" | "type">
 
 export const SessionTable = sqliteTable(
   "session",
@@ -94,6 +96,27 @@ export const TodoTable = sqliteTable(
   ],
 )
 
+/*
+export const SessionEntryTable = sqliteTable(
+  "session_entry",
+  {
+    id: text().$type<SessionEntry.ID>().primaryKey(),
+    session_id: text()
+      .$type<SessionID>()
+      .notNull()
+      .references(() => SessionTable.id, { onDelete: "cascade" }),
+    type: text().notNull(),
+    ...Timestamps,
+    data: text({ mode: "json" }).notNull().$type<SessionEntry.Entry>(),
+  },
+  (table) => [
+    index("session_entry_session_idx").on(table.session_id),
+    index("session_entry_session_type_idx").on(table.session_id, table.type),
+    index("session_entry_time_created_idx").on(table.time_created),
+  ],
+)
+*/
+
 export const PermissionTable = sqliteTable("permission", {
   project_id: text()
     .primaryKey()

+ 0 - 115
packages/opencode/src/v2/message.ts

@@ -1,115 +0,0 @@
-import { Identifier } from "@/id/id"
-import { withStatics } from "@/util/schema"
-import { DateTime, Effect, Schema } from "effect"
-
-export namespace Message {
-  export const ID = Schema.String.pipe(Schema.brand("Message.ID")).pipe(
-    withStatics((s) => ({
-      create: () => s.make(Identifier.ascending("message")),
-      prefix: "msg",
-    })),
-  )
-
-  export class Source extends Schema.Class<Source>("Message.Source")({
-    start: Schema.Number,
-    end: Schema.Number,
-    text: Schema.String,
-  }) {}
-
-  export class FileAttachment extends Schema.Class<FileAttachment>("Message.File.Attachment")({
-    uri: Schema.String,
-    mime: Schema.String,
-    name: Schema.String.pipe(Schema.optional),
-    description: Schema.String.pipe(Schema.optional),
-    source: Source.pipe(Schema.optional),
-  }) {
-    static create(url: string) {
-      return new FileAttachment({
-        uri: url,
-        mime: "text/plain",
-      })
-    }
-  }
-
-  export class AgentAttachment extends Schema.Class<AgentAttachment>("Message.Agent.Attachment")({
-    name: Schema.String,
-    source: Source.pipe(Schema.optional),
-  }) {}
-
-  export class User extends Schema.Class<User>("Message.User")({
-    id: ID,
-    type: Schema.Literal("user"),
-    text: Schema.String,
-    files: Schema.Array(FileAttachment).pipe(Schema.optional),
-    agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
-    time: Schema.Struct({
-      created: Schema.DateTimeUtc,
-    }),
-  }) {
-    static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) {
-      const msg = new User({
-        id: ID.create(),
-        type: "user",
-        ...input,
-        time: {
-          created: Effect.runSync(DateTime.now),
-        },
-      })
-      return msg
-    }
-  }
-
-  export class Synthetic extends Schema.Class<Synthetic>("Message.Synthetic")({
-    id: ID,
-    type: Schema.Literal("synthetic"),
-    text: Schema.String,
-    time: Schema.Struct({
-      created: Schema.DateTimeUtc,
-    }),
-  }) {}
-
-  export class Request extends Schema.Class<Request>("Message.Request")({
-    id: ID,
-    type: Schema.Literal("start"),
-    model: Schema.Struct({
-      id: Schema.String,
-      providerID: Schema.String,
-      variant: Schema.String.pipe(Schema.optional),
-    }),
-    time: Schema.Struct({
-      created: Schema.DateTimeUtc,
-    }),
-  }) {}
-
-  export class Text extends Schema.Class<Text>("Message.Text")({
-    id: ID,
-    type: Schema.Literal("text"),
-    text: Schema.String,
-    time: Schema.Struct({
-      created: Schema.DateTimeUtc,
-      completed: Schema.DateTimeUtc.pipe(Schema.optional),
-    }),
-  }) {}
-
-  export class Complete extends Schema.Class<Complete>("Message.Complete")({
-    id: ID,
-    type: Schema.Literal("complete"),
-    time: Schema.Struct({
-      created: Schema.DateTimeUtc,
-    }),
-    cost: Schema.Number,
-    tokens: Schema.Struct({
-      total: Schema.Number,
-      input: Schema.Number,
-      output: Schema.Number,
-      reasoning: Schema.Number,
-      cache: Schema.Struct({
-        read: Schema.Number,
-        write: Schema.Number,
-      }),
-    }),
-  }) {}
-
-  export const Info = Schema.Union([User, Text])
-  export type Info = Schema.Schema.Type<typeof Info>
-}

+ 186 - 0
packages/opencode/src/v2/session-entry.ts

@@ -0,0 +1,186 @@
+import { Identifier } from "@/id/id"
+import { withStatics } from "@/util/schema"
+import { DateTime, Effect, Schema } from "effect"
+
+export namespace SessionEntry {
+  export const ID = Schema.String.pipe(Schema.brand("Session.Entry.ID")).pipe(
+    withStatics((s) => ({
+      create: () => s.make(Identifier.ascending("entry")),
+      prefix: "ent",
+    })),
+  )
+  export type ID = Schema.Schema.Type<typeof ID>
+
+  const Base = {
+    id: ID,
+    metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+    time: Schema.Struct({
+      created: Schema.DateTimeUtc,
+    }),
+  }
+
+  export class Source extends Schema.Class<Source>("Session.Entry.Source")({
+    start: Schema.Number,
+    end: Schema.Number,
+    text: Schema.String,
+  }) {}
+
+  export class FileAttachment extends Schema.Class<FileAttachment>("Session.Entry.File.Attachment")({
+    uri: Schema.String,
+    mime: Schema.String,
+    name: Schema.String.pipe(Schema.optional),
+    description: Schema.String.pipe(Schema.optional),
+    source: Source.pipe(Schema.optional),
+  }) {
+    static create(url: string) {
+      return new FileAttachment({
+        uri: url,
+        mime: "text/plain",
+      })
+    }
+  }
+
+  export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Entry.Agent.Attachment")({
+    name: Schema.String,
+    source: Source.pipe(Schema.optional),
+  }) {}
+
+  export class User extends Schema.Class<User>("Session.Entry.User")({
+    ...Base,
+    type: Schema.Literal("user"),
+    text: Schema.String,
+    files: Schema.Array(FileAttachment).pipe(Schema.optional),
+    agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
+  }) {
+    static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) {
+      const msg = new User({
+        id: ID.create(),
+        type: "user",
+        ...input,
+        time: {
+          created: Effect.runSync(DateTime.now),
+        },
+      })
+      return msg
+    }
+  }
+
+  export class Synthetic extends Schema.Class<Synthetic>("Session.Entry.Synthetic")({
+    ...Base,
+    type: Schema.Literal("synthetic"),
+    text: Schema.String,
+  }) {}
+
+  export class Request extends Schema.Class<Request>("Session.Entry.Request")({
+    ...Base,
+    type: Schema.Literal("start"),
+    model: Schema.Struct({
+      id: Schema.String,
+      providerID: Schema.String,
+      variant: Schema.String.pipe(Schema.optional),
+    }),
+  }) {}
+
+  export class Text extends Schema.Class<Text>("Session.Entry.Text")({
+    ...Base,
+    type: Schema.Literal("text"),
+    text: Schema.String,
+    time: Schema.Struct({
+      ...Base.time.fields,
+      completed: Schema.DateTimeUtc.pipe(Schema.optional),
+    }),
+  }) {}
+
+  export class Reasoning extends Schema.Class<Reasoning>("Session.Entry.Reasoning")({
+    ...Base,
+    type: Schema.Literal("reasoning"),
+    text: Schema.String,
+    time: Schema.Struct({
+      ...Base.time.fields,
+      completed: Schema.DateTimeUtc.pipe(Schema.optional),
+    }),
+  }) {}
+
+  export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Entry.ToolState.Pending")({
+    status: Schema.Literal("pending"),
+    input: Schema.Record(Schema.String, Schema.Unknown),
+    raw: Schema.String,
+  }) {}
+
+  export class ToolStateRunning extends Schema.Class<ToolStateRunning>("Session.Entry.ToolState.Running")({
+    status: Schema.Literal("running"),
+    input: Schema.Record(Schema.String, Schema.Unknown),
+    title: Schema.String.pipe(Schema.optional),
+    metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+  }) {}
+
+  export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Session.Entry.ToolState.Completed")({
+    status: Schema.Literal("completed"),
+    input: Schema.Record(Schema.String, Schema.Unknown),
+    output: Schema.String,
+    title: Schema.String,
+    metadata: Schema.Record(Schema.String, Schema.Unknown),
+    attachments: Schema.Array(FileAttachment).pipe(Schema.optional),
+  }) {}
+
+  export class ToolStateError extends Schema.Class<ToolStateError>("Session.Entry.ToolState.Error")({
+    status: Schema.Literal("error"),
+    input: Schema.Record(Schema.String, Schema.Unknown),
+    error: Schema.String,
+    metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+    time: Schema.Struct({
+      start: Schema.Number,
+      end: Schema.Number,
+    }),
+  }) {}
+
+  export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
+  export type ToolState = Schema.Schema.Type<typeof ToolState>
+
+  export class Tool extends Schema.Class<Tool>("Session.Entry.Tool")({
+    ...Base,
+    type: Schema.Literal("tool"),
+    callID: Schema.String,
+    name: Schema.String,
+    state: ToolState,
+    time: Schema.Struct({
+      ...Base.time.fields,
+      ran: Schema.DateTimeUtc.pipe(Schema.optional),
+      completed: Schema.DateTimeUtc.pipe(Schema.optional),
+      pruned: Schema.DateTimeUtc.pipe(Schema.optional),
+    }),
+  }) {}
+
+  export class Complete extends Schema.Class<Complete>("Session.Entry.Complete")({
+    ...Base,
+    type: Schema.Literal("complete"),
+    cost: Schema.Number,
+    reason: Schema.String,
+    tokens: Schema.Struct({
+      input: Schema.Number,
+      output: Schema.Number,
+      reasoning: Schema.Number,
+      cache: Schema.Struct({
+        read: Schema.Number,
+        write: Schema.Number,
+      }),
+    }),
+  }) {}
+
+  export class Retry extends Schema.Class<Retry>("Session.Entry.Retry")({
+    ...Base,
+    type: Schema.Literal("retry"),
+    attempt: Schema.Number,
+    error: Schema.String,
+  }) {}
+
+  export class Compaction extends Schema.Class<Compaction>("Session.Entry.Compaction")({
+    ...Base,
+    type: Schema.Literal("compaction"),
+    auto: Schema.Boolean,
+    overflow: Schema.Boolean.pipe(Schema.optional),
+  }) {}
+
+  export const Entry = Schema.Union([User, Synthetic, Request, Tool, Text, Reasoning, Complete, Retry, Compaction])
+  export type Entry = Schema.Schema.Type<typeof Entry>
+}

+ 4 - 4
packages/opencode/src/v2/session.ts

@@ -1,5 +1,5 @@
 import { Context, Layer, Schema, Effect } from "effect"
-import { Message } from "./message"
+import { SessionEntry } from "./session-entry"
 import { Struct } from "effect"
 import { Identifier } from "@/id/id"
 import { withStatics } from "@/util/schema"
@@ -12,8 +12,8 @@ export namespace SessionV2 {
   export type ID = Schema.Schema.Type<typeof ID>
 
   export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
-    ...Struct.omit(Message.User.fields, ["time", "type"]),
-    id: Schema.optionalKey(Message.ID),
+    ...Struct.omit(SessionEntry.User.fields, ["time", "type"]),
+    id: Schema.optionalKey(SessionEntry.ID),
     sessionID: SessionV2.ID,
   }) {}
 
@@ -33,7 +33,7 @@ export namespace SessionV2 {
   export interface Interface {
     fromID: (id: SessionV2.ID) => Effect.Effect<Info>
     create: (input: CreateInput) => Effect.Effect<Info>
-    prompt: (input: PromptInput) => Effect.Effect<Message.User>
+    prompt: (input: PromptInput) => Effect.Effect<SessionEntry.User>
   }
 
   export class Service extends Context.Service<Service, Interface>()("Session.Service") {}