diff --git a/packages/core/package.json b/packages/core/package.json index be6c96b84..848a99fe2 100644 --- a/packages/core/package.json +++ b/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" }, diff --git a/packages/core/src/opencode.ts b/packages/core/src/opencode.ts deleted file mode 100644 index 7c7191642..000000000 --- a/packages/core/src/opencode.ts +++ /dev/null @@ -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()("@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. diff --git a/packages/core/src/public/agent.ts b/packages/core/src/public/agent.ts new file mode 100644 index 000000000..ade2096f8 --- /dev/null +++ b/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 diff --git a/packages/core/src/public/index.ts b/packages/core/src/public/index.ts new file mode 100644 index 000000000..0fd1b9537 --- /dev/null +++ b/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" diff --git a/packages/core/src/public/location.ts b/packages/core/src/public/location.ts new file mode 100644 index 000000000..aab15181d --- /dev/null +++ b/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 diff --git a/packages/core/src/public/model.ts b/packages/core/src/public/model.ts new file mode 100644 index 000000000..ab92b8dfe --- /dev/null +++ b/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 diff --git a/packages/core/src/public/opencode.ts b/packages/core/src/public/opencode.ts new file mode 100644 index 000000000..67b26b00c --- /dev/null +++ b/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()("@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. diff --git a/packages/core/src/public/session.ts b/packages/core/src/public/session.ts new file mode 100644 index 000000000..f66fe2b08 --- /dev/null +++ b/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 + +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 + readonly get: (sessionID: ID) => Effect.Effect + readonly list: (input?: ListInput) => Effect.Effect + readonly prompt: (input: PromptInput) => Effect.Effect + readonly messages: (input: MessagesInput) => Effect.Effect + readonly message: (input: MessageInput) => Effect.Effect + readonly context: (sessionID: ID) => Effect.Effect + readonly events: (input: EventsInput) => Stream.Stream +} diff --git a/packages/core/test/opencode.test.ts b/packages/core/test/opencode.test.ts deleted file mode 100644 index 3c0c499f5..000000000 --- a/packages/core/test/opencode.test.ts +++ /dev/null @@ -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() - }), - ) -}) diff --git a/packages/core/test/public-opencode.test.ts b/packages/core/test/public-opencode.test.ts new file mode 100644 index 000000000..c74421218 --- /dev/null +++ b/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() + }), + ) + +})