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

This commit is contained in:
Kit Langton 2026-06-04 20:49:43 -04:00 committed by GitHub
parent 46e9863589
commit 773d33e282
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 220 additions and 55 deletions

View File

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

View File

@ -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.

View File

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

View File

@ -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"

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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>
}

View File

@ -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()
}),
)
})

View File

@ -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()
}),
)
})