plugin.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. export * as PluginV2 from "./plugin"
  2. import { createDraft, finishDraft, type Draft } from "immer"
  3. import type { LanguageModelV3 } from "@ai-sdk/provider"
  4. import { type ProviderV2 } from "./provider"
  5. import { Context, Effect, Layer, Schema } from "effect"
  6. import type { ModelV2 } from "./model"
  7. export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
  8. export type ID = typeof ID.Type
  9. type HookSpec = {
  10. "provider.update": {
  11. input: {}
  12. output: {
  13. provider: ProviderV2.Info
  14. cancel: boolean
  15. }
  16. }
  17. "model.update": {
  18. input: {}
  19. output: {
  20. model: ModelV2.Info
  21. cancel: boolean
  22. }
  23. }
  24. "aisdk.language": {
  25. input: {
  26. model: ModelV2.Info
  27. sdk: any
  28. options: Record<string, any>
  29. }
  30. output: {
  31. language?: LanguageModelV3
  32. }
  33. }
  34. "aisdk.sdk": {
  35. input: {
  36. model: ModelV2.Info
  37. package: string
  38. options: Record<string, any>
  39. }
  40. output: {
  41. sdk?: any
  42. }
  43. }
  44. }
  45. export type Hooks = {
  46. [Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
  47. -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
  48. ? Draft<HookSpec[Name]["output"][Field]>
  49. : HookSpec[Name]["output"][Field]
  50. }
  51. }
  52. export type HookFunctions = {
  53. [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
  54. }
  55. export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
  56. export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
  57. export type Effect = Effect.Effect<HookFunctions | void, never, never>
  58. export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
  59. return input
  60. }
  61. export interface Interface {
  62. readonly add: (input: { id: ID; effect: Effect }) => Effect.Effect<void>
  63. readonly remove: (id: ID) => Effect.Effect<void>
  64. readonly trigger: <Name extends keyof Hooks>(
  65. name: Name,
  66. input: HookInput<Name>,
  67. output: HookOutput<Name>,
  68. ) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
  69. }
  70. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
  71. export const layer = Layer.effect(
  72. Service,
  73. Effect.gen(function* () {
  74. let hooks: {
  75. id: ID
  76. hooks: HookFunctions
  77. }[] = []
  78. const svc = Service.of({
  79. add: Effect.fn("Plugin.add")(function* (input) {
  80. const result = yield* input.effect
  81. if (!result) return
  82. hooks = [
  83. ...hooks.filter((item) => item.id !== input.id),
  84. {
  85. id: input.id,
  86. hooks: result,
  87. },
  88. ]
  89. }),
  90. trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
  91. const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
  92. const event = {
  93. ...input,
  94. ...output,
  95. } as Record<string, unknown>
  96. for (const [field, value] of Object.entries(output)) {
  97. if (value && typeof value === "object") {
  98. draftEntries.set(field, createDraft(value))
  99. event[field] = draftEntries.get(field)
  100. }
  101. }
  102. for (const item of hooks) {
  103. const match = item.hooks[name]
  104. if (!match) continue
  105. yield* match(event as any).pipe(
  106. Effect.withSpan(`Plugin.hook.${name}`, {
  107. attributes: {
  108. plugin: item.id,
  109. hook: name,
  110. },
  111. }),
  112. )
  113. }
  114. for (const [field, draft] of draftEntries) {
  115. event[field] = finishDraft(draft)
  116. }
  117. return event as any
  118. }),
  119. remove: Effect.fn("Plugin.remove")(function* (id) {
  120. hooks = hooks.filter((item) => item.id !== id)
  121. }),
  122. })
  123. return svc
  124. }),
  125. )
  126. export const defaultLayer = layer
  127. // opencode
  128. // sdcok