plugin.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. export * as PluginV2 from "./plugin"
  2. import { createDraft, finishDraft, type Draft } from "immer"
  3. import type { LanguageModelV3 } from "@ai-sdk/provider"
  4. import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
  5. import type { ModelV2 } from "./model"
  6. import type { Catalog } from "./catalog"
  7. import { EventV2 } from "./event"
  8. import { KeyedMutex } from "./effect/keyed-mutex"
  9. export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
  10. export type ID = typeof ID.Type
  11. export const Event = {
  12. Added: EventV2.define({
  13. type: "plugin.added",
  14. schema: {
  15. id: ID,
  16. },
  17. }),
  18. }
  19. type HookSpec = {
  20. "catalog.transform": {
  21. input: Catalog.Editor
  22. output: {}
  23. }
  24. "account.switched": {
  25. input: {
  26. serviceID: import("./auth").Auth.ServiceID
  27. from?: import("./auth").Auth.ID
  28. to?: import("./auth").Auth.ID
  29. }
  30. output: {}
  31. }
  32. "aisdk.language": {
  33. input: {
  34. model: ModelV2.Info
  35. sdk: any
  36. options: Record<string, any>
  37. }
  38. output: {
  39. language?: LanguageModelV3
  40. }
  41. }
  42. "aisdk.sdk": {
  43. input: {
  44. model: ModelV2.Info
  45. package: string
  46. options: Record<string, any>
  47. }
  48. output: {
  49. sdk?: any
  50. }
  51. }
  52. }
  53. export type Hooks = {
  54. [Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
  55. -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
  56. ? Draft<HookSpec[Name]["output"][Field]>
  57. : HookSpec[Name]["output"][Field]
  58. }
  59. }
  60. export type HookFunctions = {
  61. [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
  62. }
  63. export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
  64. export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
  65. export type Effect<R = never> = Effect.Effect<HookFunctions | void, never, R | Scope.Scope>
  66. export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
  67. return input
  68. }
  69. export interface Interface {
  70. readonly add: (input: {
  71. id: ID
  72. effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
  73. }) => Effect.Effect<void, never, never>
  74. readonly remove: (id: ID) => Effect.Effect<void>
  75. readonly triggerFor: <Name extends keyof Hooks>(
  76. id: ID,
  77. name: Name,
  78. input: HookInput<Name>,
  79. output: HookOutput<Name>,
  80. ) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
  81. readonly trigger: <Name extends keyof Hooks>(
  82. name: Name,
  83. input: HookInput<Name>,
  84. output: HookOutput<Name>,
  85. ) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
  86. }
  87. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
  88. export const layer = Layer.effect(
  89. Service,
  90. Effect.gen(function* () {
  91. let hooks: {
  92. id: ID
  93. hooks: HookFunctions
  94. scope: Scope.Closeable
  95. }[] = []
  96. const events = yield* EventV2.Service
  97. const scope = yield* Scope.Scope
  98. const locks = KeyedMutex.makeUnsafe<ID>()
  99. const svc = Service.of({
  100. add: Effect.fn("Plugin.add")(function* (input) {
  101. yield* locks.withLock(input.id)(
  102. Effect.gen(function* () {
  103. const existing = hooks.find((item) => item.id === input.id)
  104. if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
  105. const childScope = yield* Scope.fork(scope)
  106. const result = yield* input.effect.pipe(
  107. Scope.provide(childScope),
  108. Effect.withSpan("Plugin.load", {
  109. attributes: {
  110. "plugin.id": input.id,
  111. },
  112. }),
  113. Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
  114. )
  115. hooks = [
  116. ...hooks.filter((item) => item.id !== input.id),
  117. {
  118. id: input.id,
  119. hooks: result ?? {},
  120. scope: childScope,
  121. },
  122. ]
  123. yield* events.publish(Event.Added, { id: input.id })
  124. }),
  125. )
  126. }),
  127. trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
  128. return yield* svc.triggerFor(ID.make("*"), name, input, output)
  129. }),
  130. triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) {
  131. const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
  132. const event = {
  133. ...input,
  134. ...output,
  135. } as Record<string, unknown>
  136. for (const [field, value] of Object.entries(output)) {
  137. if (value && typeof value === "object") {
  138. draftEntries.set(field, createDraft(value))
  139. event[field] = draftEntries.get(field)
  140. }
  141. }
  142. for (const item of hooks) {
  143. if (id !== ID.make("*") && item.id !== id) continue
  144. const match = item.hooks[name]
  145. if (!match) continue
  146. yield* match(event as any).pipe(
  147. Effect.withSpan(`Plugin.hook.${name}`, {
  148. attributes: {
  149. plugin: item.id,
  150. hook: name,
  151. },
  152. }),
  153. )
  154. }
  155. for (const [field, draft] of draftEntries) {
  156. event[field] = finishDraft(draft)
  157. }
  158. return event as any
  159. }),
  160. remove: Effect.fn("Plugin.remove")(function* (id) {
  161. yield* locks.withLock(id)(
  162. Effect.gen(function* () {
  163. const existing = hooks.find((item) => item.id === id)
  164. hooks = hooks.filter((item) => item.id !== id)
  165. if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
  166. }),
  167. )
  168. }),
  169. })
  170. return svc
  171. }),
  172. )
  173. export const locationLayer = layer
  174. // opencode
  175. // sdcok