Sfoglia il codice sorgente

feat(core): add public native API (#30828)

Kit Langton 2 mesi fa
parent
commit
773d33e282

+ 1 - 0
packages/core/package.json

@@ -17,6 +17,7 @@
     "opencode": "./bin/opencode"
   },
   "exports": {
+    "./public": "./src/public/index.ts",
     "./session/runner": "./src/session/runner/index.ts",
     "./*": "./src/*.ts"
   },

+ 0 - 39
packages/core/src/opencode.ts

@@ -1,39 +0,0 @@
-export * as OpenCode from "./opencode"
-
-import { Context, Effect, Layer } from "effect"
-import { Database } from "./database/database"
-import { EventV2 } from "./event"
-import { LocationServiceMap } from "./location-layer"
-import { ProjectV2 } from "./project"
-import { SessionV2 } from "./session"
-import { SessionProjector } from "./session/projector"
-import * as SessionExecutionLocal from "./session/execution/local"
-import { SessionStore } from "./session/store"
-
-export interface Interface {
-  readonly sessions: SessionV2.Interface
-}
-
-/** Public embedded OpenCode API for Effect-native applications. */
-export class Service extends Context.Service<Service, Interface>()("@opencode/OpenCode") {}
-
-const DefaultSessions = 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,
-)
-
-// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
-export const layer = Layer.effect(
-  Service,
-  Effect.gen(function* () {
-    return Service.of({ sessions: yield* SessionV2.Service })
-  }),
-).pipe(Layer.provide(DefaultSessions))
-
-// TODO: Add OpenCode.create(...) as the Promise facade over the same embedded API semantics.

+ 6 - 0
packages/core/src/public/agent.ts

@@ -0,0 +1,6 @@
+export * as Agent from "./agent"
+
+import { AgentV2 } from "../agent"
+
+export const ID = AgentV2.ID
+export type ID = AgentV2.ID

+ 8 - 0
packages/core/src/public/index.ts

@@ -0,0 +1,8 @@
+/** Intentional supported native API. Other core subpaths remain internal implementation surfaces. */
+export { Agent } from "./agent"
+export { Model } from "./model"
+export { OpenCode } from "./opencode"
+export { Session } from "./session"
+export { Location } from "./location"
+export { Prompt } from "../session/prompt"
+export { AbsolutePath } from "../schema"

+ 6 - 0
packages/core/src/public/location.ts

@@ -0,0 +1,6 @@
+export * as Location from "./location"
+
+import { Location } from "../location"
+
+export const Ref = Location.Ref
+export type Ref = Location.Ref

+ 9 - 0
packages/core/src/public/model.ts

@@ -0,0 +1,9 @@
+export * as Model from "./model"
+
+import { ModelV2 } from "../model"
+
+export const ID = ModelV2.ID
+export type ID = ModelV2.ID
+
+export const Ref = ModelV2.Ref
+export type Ref = ModelV2.Ref

+ 70 - 0
packages/core/src/public/opencode.ts

@@ -0,0 +1,70 @@
+export * as OpenCode from "./opencode"
+
+import { Context, Effect, Layer } from "effect"
+import { Database } from "../database/database"
+import { EventV2 } from "../event"
+import { LocationServiceMap } from "../location-layer"
+import { ProjectV2 } from "../project"
+import { SessionV2 } from "../session"
+import * as SessionExecutionLocal from "../session/execution/local"
+import { SessionProjector } from "../session/projector"
+import { SessionStore } from "../session/store"
+import { Session } from "./session"
+
+export interface Interface {
+  readonly sessions: Session.Interface
+}
+
+/** Intentional public native API for Effect applications embedding OpenCode. */
+export class Service extends Context.Service<Service, Interface>()("@opencode/public/OpenCode") {}
+
+const SessionsLayer = 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,
+)
+
+// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const sessions = yield* SessionV2.Service
+    return Service.of({
+      sessions: {
+        create: (input) =>
+          sessions.create({
+            id: input.id,
+            agent: input.agent,
+            model: input.model,
+            location: input.location,
+        }),
+        get: sessions.get,
+        list: sessions.list,
+        prompt: (input) =>
+          sessions.prompt({
+            id: input.id,
+            sessionID: input.sessionID,
+            prompt: input.prompt,
+            delivery: input.delivery,
+          }),
+        messages: (input) =>
+          sessions.messages({
+            sessionID: input.sessionID,
+            limit: input.limit,
+            order: input.order,
+            cursor: input.cursor,
+          }),
+        message: (input) => sessions.message({ sessionID: input.sessionID, messageID: input.messageID }),
+        context: sessions.context,
+        events: (input) => sessions.events({ sessionID: input.sessionID, after: input.after }),
+      },
+    })
+  }),
+).pipe(Layer.provide(SessionsLayer))
+
+// TODO: Add OpenCode.create(...) as the Promise facade over the same native API semantics.

+ 91 - 0
packages/core/src/public/session.ts

@@ -0,0 +1,91 @@
+export * as Session from "./session"
+
+import { Effect, Stream } from "effect"
+import { EventV2 } from "../event"
+import { SessionV2 } from "../session"
+import { MessageDecodeError } from "../session/error"
+import { SessionEvent } from "../session/event"
+import { SessionInput } from "../session/input"
+import { SessionMessage } from "../session/message"
+import { Prompt } from "../session/prompt"
+import { Agent } from "./agent"
+import { Location } from "./location"
+import { Model } from "./model"
+
+export const ID = SessionV2.ID
+export type ID = SessionV2.ID
+
+export const Info = SessionV2.Info
+export type Info = SessionV2.Info
+
+export const MessageID = SessionMessage.ID
+export type MessageID = SessionMessage.ID
+
+export const Message = SessionMessage.Message
+export type Message = SessionMessage.Message
+
+export const Admission = SessionInput.Admitted
+export type Admission = SessionInput.Admitted
+
+export const Delivery = SessionInput.Delivery
+export type Delivery = SessionInput.Delivery
+
+export const ListInput = SessionV2.ListInput
+export type ListInput = SessionV2.ListInput
+
+export const EventCursor = EventV2.Cursor
+export type EventCursor = EventV2.Cursor
+export type Event = EventV2.CursorEvent<SessionEvent.DurableEvent>
+
+export const NotFoundError = SessionV2.NotFoundError
+export type NotFoundError = SessionV2.NotFoundError
+
+export const PromptConflictError = SessionV2.PromptConflictError
+export type PromptConflictError = SessionV2.PromptConflictError
+
+export { MessageDecodeError }
+
+export interface CreateInput {
+  readonly id?: ID
+  readonly agent?: Agent.ID
+  readonly model?: Model.Ref
+  readonly location: Location.Ref
+}
+
+export interface PromptInput {
+  readonly id?: MessageID
+  readonly sessionID: ID
+  readonly prompt: Prompt
+  readonly delivery?: Delivery
+}
+
+export interface MessagesInput {
+  readonly sessionID: ID
+  readonly limit?: number
+  readonly order?: "asc" | "desc"
+  readonly cursor?: {
+    readonly id: MessageID
+    readonly direction: "previous" | "next"
+  }
+}
+
+export interface MessageInput {
+  readonly sessionID: ID
+  readonly messageID: MessageID
+}
+
+export interface EventsInput {
+  readonly sessionID: ID
+  readonly after?: EventCursor
+}
+
+export interface Interface {
+  readonly create: (input: CreateInput) => Effect.Effect<Info>
+  readonly get: (sessionID: ID) => Effect.Effect<Info, NotFoundError>
+  readonly list: (input?: ListInput) => Effect.Effect<Info[]>
+  readonly prompt: (input: PromptInput) => Effect.Effect<Admission, NotFoundError | PromptConflictError>
+  readonly messages: (input: MessagesInput) => Effect.Effect<Message[], NotFoundError | MessageDecodeError>
+  readonly message: (input: MessageInput) => Effect.Effect<Message | undefined>
+  readonly context: (sessionID: ID) => Effect.Effect<Message[], NotFoundError | MessageDecodeError>
+  readonly events: (input: EventsInput) => Stream.Stream<Event, NotFoundError>
+}

+ 0 - 16
packages/core/test/opencode.test.ts

@@ -1,16 +0,0 @@
-import { describe, expect } from "bun:test"
-import { Effect } from "effect"
-import { OpenCode } from "@opencode-ai/core/opencode"
-import { testEffect } from "./lib/effect"
-
-const it = testEffect(OpenCode.layer)
-
-describe("OpenCode.layer", () => {
-  it.effect("exposes Sessions through the public embedded API", () =>
-    Effect.gen(function* () {
-      const opencode = yield* OpenCode.Service
-
-      expect(yield* opencode.sessions.list()).toBeArray()
-    }),
-  )
-})

+ 29 - 0
packages/core/test/public-opencode.test.ts

@@ -0,0 +1,29 @@
+import { describe, expect } from "bun:test"
+import { Effect } from "effect"
+import { OpenCode, Session } from "@opencode-ai/core/public"
+import { testEffect } from "./lib/effect"
+
+const it = testEffect(OpenCode.layer)
+
+describe("public native OpenCode API", () => {
+  it.effect("exposes only the intentional Session capabilities", () =>
+    Effect.gen(function* () {
+      const opencode = yield* OpenCode.Service
+
+      expect(Object.keys(opencode.sessions).sort()).toEqual([
+        "context",
+        "create",
+        "events",
+        "get",
+        "list",
+        "message",
+        "messages",
+        "prompt",
+      ])
+      expect(Session.ID.create()).toStartWith("ses_")
+      expect(Session.MessageID.create()).toStartWith("msg_")
+      expect(yield* opencode.sessions.list()).toBeArray()
+    }),
+  )
+
+})