plugin.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. export * as PluginV2 from "./plugin"
  2. import { makeLocationNode } from "./effect/app-node"
  3. import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
  4. import type { Plugin as PluginRuntime } from "@kirincode-ai/plugin/v2/effect"
  5. import { Plugin } from "@kirincode-ai/schema/plugin"
  6. import { AgentV2 } from "./agent"
  7. import { AISDK } from "./aisdk"
  8. import { Catalog } from "./catalog"
  9. import { CommandV2 } from "./command"
  10. import { EventV2 } from "./event"
  11. import { Integration } from "./integration"
  12. import { KeyedMutex } from "./effect/keyed-mutex"
  13. import { PluginHost } from "./plugin/host"
  14. import { Reference } from "./reference"
  15. import { SkillV2 } from "./skill"
  16. import { State } from "./state"
  17. export const ID = Plugin.ID
  18. export type ID = typeof ID.Type
  19. export const Event = Plugin.Event
  20. export interface Interface {
  21. readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect<void>
  22. readonly remove: (id: ID) => Effect.Effect<void>
  23. readonly wait: (id: ID) => Effect.Effect<void>
  24. }
  25. export class Service extends Context.Service<Service, Interface>()("@kirincode/v2/Plugin") {}
  26. const layer = Layer.effect(
  27. Service,
  28. Effect.gen(function* () {
  29. const events = yield* EventV2.Service
  30. const locks = KeyedMutex.makeUnsafe<ID>()
  31. const scope = yield* Scope.make()
  32. const active = new Map<ID, Scope.Closeable>()
  33. const loading = new Set<ID>()
  34. const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
  35. const failures = new Map<ID, Exit.Exit<void, never>>()
  36. let host: Parameters<PluginRuntime["effect"]>[0]
  37. const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) {
  38. if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
  39. yield* locks.withLock(id)(
  40. Effect.sync(() => {
  41. loading.add(id)
  42. failures.delete(id)
  43. }).pipe(
  44. Effect.andThen(
  45. State.batch(
  46. Effect.gen(function* () {
  47. const existing = active.get(id)
  48. active.delete(id)
  49. if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
  50. const child = yield* Scope.fork(scope)
  51. yield* effect(host).pipe(
  52. Scope.provide(child),
  53. Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
  54. Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
  55. )
  56. yield* events.publish(Event.Added, { id })
  57. active.set(id, child)
  58. yield* Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.succeed(waiter, undefined), {
  59. discard: true,
  60. })
  61. waiters.delete(id)
  62. }),
  63. ),
  64. ),
  65. Effect.onExit((exit) => {
  66. if (Exit.isSuccess(exit)) return Effect.void
  67. failures.set(id, exit)
  68. return Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.done(waiter, exit), {
  69. discard: true,
  70. }).pipe(Effect.ensuring(Effect.sync(() => waiters.delete(id))))
  71. }),
  72. Effect.ensuring(Effect.sync(() => loading.delete(id))),
  73. ),
  74. )
  75. })
  76. const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
  77. if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
  78. yield* locks.withLock(id)(
  79. State.batch(
  80. Effect.gen(function* () {
  81. const current = active.get(id)
  82. active.delete(id)
  83. failures.delete(id)
  84. if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
  85. }),
  86. ),
  87. )
  88. })
  89. const wait = Effect.fn("Plugin.wait")(function* (id: ID) {
  90. const waiter = yield* Deferred.make<void>()
  91. const pending = yield* locks.withLock(id)(
  92. Effect.sync(() => {
  93. if (active.has(id)) return false
  94. const failure = failures.get(id)
  95. if (failure) return failure
  96. const current = waiters.get(id) ?? new Set()
  97. current.add(waiter)
  98. waiters.set(id, current)
  99. return true
  100. }),
  101. )
  102. if (!pending) return
  103. if (typeof pending !== "boolean") return yield* pending
  104. yield* Deferred.await(waiter).pipe(
  105. Effect.ensuring(
  106. locks.withLock(id)(
  107. Effect.sync(() => {
  108. const current = waiters.get(id)
  109. current?.delete(waiter)
  110. if (current?.size === 0) waiters.delete(id)
  111. }),
  112. ),
  113. ),
  114. )
  115. })
  116. yield* Effect.addFinalizer((exit) =>
  117. Effect.gen(function* () {
  118. active.clear()
  119. yield* State.batch(Scope.close(scope, exit))
  120. }),
  121. )
  122. const service = Service.of({
  123. add,
  124. remove,
  125. wait,
  126. })
  127. host = yield* PluginHost.make(service)
  128. return service
  129. }),
  130. )
  131. export const locationLayer = layer.pipe(
  132. Layer.provideMerge(AgentV2.locationLayer),
  133. Layer.provideMerge(AISDK.locationLayer),
  134. Layer.provideMerge(Catalog.locationLayer),
  135. Layer.provideMerge(CommandV2.locationLayer),
  136. Layer.provideMerge(Integration.locationLayer),
  137. Layer.provideMerge(Reference.locationLayer),
  138. )
  139. export const node = makeLocationNode({
  140. service: Service,
  141. layer,
  142. deps: [
  143. EventV2.node,
  144. AgentV2.node,
  145. AISDK.node,
  146. Catalog.node,
  147. CommandV2.node,
  148. Integration.node,
  149. Reference.node,
  150. SkillV2.node,
  151. ],
  152. })