plugin.ts 5.3 KB

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