feat(core): add public native API (#30828)
This commit is contained in:
parent
46e9863589
commit
773d33e282
@ -17,6 +17,7 @@
|
||||
"opencode": "./bin/opencode"
|
||||
},
|
||||
"exports": {
|
||||
"./public": "./src/public/index.ts",
|
||||
"./session/runner": "./src/session/runner/index.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.
|
||||
6
packages/core/src/public/agent.ts
Normal file
6
packages/core/src/public/agent.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * as Agent from "./agent"
|
||||
|
||||
import { AgentV2 } from "../agent"
|
||||
|
||||
export const ID = AgentV2.ID
|
||||
export type ID = AgentV2.ID
|
||||
8
packages/core/src/public/index.ts
Normal file
8
packages/core/src/public/index.ts
Normal 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"
|
||||
6
packages/core/src/public/location.ts
Normal file
6
packages/core/src/public/location.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * as Location from "./location"
|
||||
|
||||
import { Location } from "../location"
|
||||
|
||||
export const Ref = Location.Ref
|
||||
export type Ref = Location.Ref
|
||||
9
packages/core/src/public/model.ts
Normal file
9
packages/core/src/public/model.ts
Normal 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
|
||||
70
packages/core/src/public/opencode.ts
Normal file
70
packages/core/src/public/opencode.ts
Normal 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.
|
||||
91
packages/core/src/public/session.ts
Normal file
91
packages/core/src/public/session.ts
Normal 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>
|
||||
}
|
||||
@ -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
packages/core/test/public-opencode.test.ts
Normal file
29
packages/core/test/public-opencode.test.ts
Normal 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()
|
||||
}),
|
||||
)
|
||||
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user