feat(core): add public native API (#30828)
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
"opencode": "./bin/opencode"
|
"opencode": "./bin/opencode"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./public": "./src/public/index.ts",
|
||||||
"./session/runner": "./src/session/runner/index.ts",
|
"./session/runner": "./src/session/runner/index.ts",
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.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.
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * as Agent from "./agent"
|
||||||
|
|
||||||
|
import { AgentV2 } from "../agent"
|
||||||
|
|
||||||
|
export const ID = AgentV2.ID
|
||||||
|
export type ID = AgentV2.ID
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * as Location from "./location"
|
||||||
|
|
||||||
|
import { Location } from "../location"
|
||||||
|
|
||||||
|
export const Ref = Location.Ref
|
||||||
|
export type Ref = Location.Ref
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -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()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user