fix(plugin): adapt promise host wire values

This commit is contained in:
Dax Raad 2026-07-10 17:18:03 -04:00
commit 20e37e7122
3 changed files with 137 additions and 19 deletions

View file

@ -193,12 +193,12 @@ import { ClientError } from "./client-error"
export interface ClientOptions {
readonly baseUrl: string
readonly fetch?: typeof globalThis.fetch
readonly headers?: HeadersInit
readonly headers?: RequestInit["headers"]
}
export interface RequestOptions {
readonly signal?: AbortSignal
readonly headers?: HeadersInit
readonly headers?: RequestInit["headers"]
}
interface RequestDescriptor {

View file

@ -2,13 +2,26 @@ export * as PluginPromise from "./promise"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { AnyTool } from "@opencode-ai/plugin/v2/tool"
import { Effect, Scope, Stream } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { AbsolutePath } from "@opencode-ai/schema/schema"
import { Session } from "@opencode-ai/schema/session"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { DateTime, Effect, Scope, Stream } from "effect"
import { Tool } from "../tool/tool"
type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
type PromisePlugin = import("@opencode-ai/plugin/v2/plugin").Plugin
type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
type PromiseEvent = ReturnType<PromisePluginContext["event"]["subscribe"]> extends AsyncIterable<infer Event>
? Event
: never
type JsonValue = null | boolean | number | string | Array<JsonValue> | { [key: string]: JsonValue }
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
@ -33,7 +46,7 @@ export function fromPromise(plugin: PromisePlugin) {
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
}))
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect)
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect).then(wire)
const transform =
<Draft>(domain: {
@ -60,11 +73,12 @@ export function fromPromise(plugin: PromisePlugin) {
catalog: {
provider: {
list: (input) => run(host.catalog.provider.list(input)),
get: (input) => run(host.catalog.provider.get(input)),
get: (input) => run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
},
model: {
list: (input) => run(host.catalog.model.list(input)),
default: (input) => run(host.catalog.model.default(input)),
default: (input) =>
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
},
transform: transform(host.catalog),
reload: () => run(host.catalog.reload()),
@ -75,19 +89,48 @@ export function fromPromise(plugin: PromisePlugin) {
reload: () => run(host.command.reload()),
},
event: {
subscribe: () => Stream.toAsyncIterable(host.event.subscribe()),
subscribe: () => Stream.toAsyncIterable(host.event.subscribe().pipe(Stream.map(wireEvent))),
},
integration: {
list: (input) => run(host.integration.list(input)),
get: (input) => run(host.integration.get(input)),
get: (input) =>
run(host.integration.get({ ...input, integrationID: Integration.ID.make(input.integrationID) })).then(
(result) => ({ ...result, data: result.data ?? null }),
),
connect: {
key: (input) => run(host.integration.connect.key(input)),
oauth: (input) => run(host.integration.connect.oauth(input)),
key: (input) =>
run(host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) })),
oauth: (input) =>
run(
host.integration.connect.oauth({
...input,
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
}),
),
},
attempt: {
status: (input) => run(host.integration.attempt.status(input)),
complete: (input) => run(host.integration.attempt.complete(input)),
cancel: (input) => run(host.integration.attempt.cancel(input)),
status: (input) =>
run(
host.integration.attempt.status({
...input,
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
complete: (input) =>
run(
host.integration.attempt.complete({
...input,
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
cancel: (input) =>
run(
host.integration.attempt.cancel({
...input,
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
},
transform: transform(host.integration),
reload: () => run(host.integration.reload()),
@ -122,11 +165,53 @@ export function fromPromise(plugin: PromisePlugin) {
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
session: {
create: (input) => run(host.session.create(input)),
get: (input) => run(host.session.get(input)),
prompt: (input) => run(host.session.prompt(input)),
command: (input) => run(host.session.command(input)),
interrupt: (input) => run(host.session.interrupt(input)),
create: (input) =>
run(
host.session.create(
input === undefined
? undefined
: {
id: input.id == null ? undefined : Session.ID.make(input.id),
agent: input.agent == null ? undefined : Agent.ID.make(input.agent),
model: input.model == null ? undefined : model(input.model),
location:
input.location == null
? undefined
: Location.Ref.make({
directory: AbsolutePath.make(input.location.directory),
workspaceID:
input.location.workspaceID === undefined
? undefined
: Workspace.ID.make(input.location.workspaceID),
}),
},
),
),
get: (input) => run(host.session.get({ sessionID: Session.ID.make(input.sessionID) })),
prompt: (input) =>
run(
host.session.prompt({
...input,
sessionID: Session.ID.make(input.sessionID),
id: input.id == null ? undefined : SessionMessage.ID.make(input.id),
delivery: input.delivery ?? undefined,
resume: input.resume ?? undefined,
}),
),
command: (input) =>
run(
host.session.command({
...input,
sessionID: Session.ID.make(input.sessionID),
id: input.id == null ? undefined : SessionMessage.ID.make(input.id),
agent: input.agent == null ? undefined : Agent.ID.make(input.agent),
model: input.model == null ? undefined : model(input.model),
arguments: input.arguments ?? undefined,
delivery: input.delivery ?? undefined,
resume: input.resume ?? undefined,
}),
),
interrupt: (input) => run(host.session.interrupt({ sessionID: Session.ID.make(input.sessionID) })),
},
}
@ -137,6 +222,39 @@ export function fromPromise(plugin: PromisePlugin) {
})
}
function model(input: { readonly id: string; readonly providerID: string; readonly variant?: string }) {
return Model.Ref.make({
id: Model.ID.make(input.id),
providerID: Provider.ID.make(input.providerID),
variant: input.variant === undefined ? undefined : Model.VariantID.make(input.variant),
})
}
type Wire<Value> = unknown extends Value
? JsonValue
: Value extends string | number | boolean | bigint | symbol | null | undefined
? Value
: Value extends DateTime.DateTime
? number
: Value extends ReadonlyArray<infer Item>
? Array<Wire<Item>>
: Value extends object
? { -readonly [Key in keyof Value]: Wire<Value[Key]> }
: Value
function wire<Value>(value: Value): Wire<Value>
function wire(value: unknown): unknown {
if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value)
if (Array.isArray(value)) return value.map(wire)
if (typeof value !== "object" || value === null) return value
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, wire(item)]))
}
function wireEvent(value: unknown): PromiseEvent
function wireEvent(value: unknown): unknown {
return wire(value)
}
function fromPromiseTool(tool: AnyTool) {
if ("jsonSchema" in tool)
return Tool.make({

File diff suppressed because one or more lines are too long