opencode.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. export * as OpenCode from "./opencode"
  2. import { Context, Effect, Layer } from "effect"
  3. import { Catalog } from "../catalog"
  4. import { Database } from "../database/database"
  5. import { EventV2 } from "../event"
  6. import { LocationServiceMap } from "../location-layer"
  7. import { PluginBoot } from "../plugin/boot"
  8. import { ProjectV2 } from "../project"
  9. import { SessionV2 } from "../session"
  10. import * as SessionExecutionLocal from "../session/execution/local"
  11. import { SessionProjector } from "../session/projector"
  12. import { SessionStore } from "../session/store"
  13. import { ApplicationTools } from "../tool/application-tools"
  14. import { Session } from "./session"
  15. import { Tool } from "./tool"
  16. export interface Interface {
  17. readonly sessions: Session.Interface
  18. readonly tools: Tool.Service
  19. }
  20. /** Intentional public native API for Effect applications embedding OpenCode. */
  21. export class Service extends Context.Service<Service, Interface>()("@opencode/public/OpenCode") {}
  22. class SessionModelValidation extends Context.Service<
  23. SessionModelValidation,
  24. {
  25. readonly validate: (
  26. input: Session.SwitchModelInput & { readonly location: Session.Info["location"] },
  27. ) => Effect.Effect<void, Session.ModelUnavailableError | Session.VariantUnavailableError>
  28. }
  29. >()("@opencode/public/OpenCode/SessionModelValidation") {}
  30. const LocationServicesLayer = LocationServiceMap.layer
  31. const SessionModelValidationLayer = Layer.effect(
  32. SessionModelValidation,
  33. Effect.gen(function* () {
  34. const locations = yield* LocationServiceMap
  35. return SessionModelValidation.of({
  36. validate: Effect.fn("OpenCode.sessions.validateModel")(function* (input) {
  37. yield* Effect.gen(function* () {
  38. yield* (yield* PluginBoot.Service).wait()
  39. const catalog = yield* Catalog.Service
  40. const model = (yield* catalog.model.available()).find(
  41. (model) => model.providerID === input.model.providerID && model.id === input.model.id,
  42. )
  43. if (!model)
  44. return yield* new Session.ModelUnavailableError({
  45. providerID: input.model.providerID,
  46. modelID: input.model.id,
  47. })
  48. if (
  49. input.model.variant !== undefined &&
  50. input.model.variant !== "default" &&
  51. !model.variants.some((variant) => variant.id === input.model.variant)
  52. )
  53. return yield* new Session.VariantUnavailableError({
  54. providerID: input.model.providerID,
  55. modelID: input.model.id,
  56. variant: input.model.variant,
  57. })
  58. }).pipe(Effect.provide(locations.get(input.location)))
  59. }),
  60. })
  61. }),
  62. )
  63. const SessionsLayer = Layer.merge(
  64. SessionV2.layer.pipe(
  65. Layer.provide(SessionProjector.layer),
  66. Layer.provide(SessionExecutionLocal.layer),
  67. Layer.provide(SessionStore.layer),
  68. Layer.provide(EventV2.layer),
  69. Layer.provide(Database.defaultLayer),
  70. Layer.provide(ProjectV2.defaultLayer),
  71. Layer.orDie,
  72. ),
  73. SessionModelValidationLayer,
  74. ).pipe(Layer.provide(LocationServicesLayer))
  75. const ApplicationToolsLayer = ApplicationTools.layer
  76. // TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
  77. export const layer = Layer.effect(
  78. Service,
  79. Effect.gen(function* () {
  80. const sessions = yield* SessionV2.Service
  81. const tools = yield* ApplicationTools.Service
  82. const validation = yield* SessionModelValidation
  83. return Service.of({
  84. tools: { attach: tools.attach },
  85. sessions: {
  86. create: (input) =>
  87. sessions.create({
  88. id: input.id,
  89. agent: input.agent,
  90. model: input.model,
  91. location: input.location,
  92. }),
  93. get: sessions.get,
  94. list: sessions.list,
  95. switchModel: Effect.fn("OpenCode.sessions.switchModel")(function* (input) {
  96. const session = yield* sessions.get(input.sessionID)
  97. yield* validation.validate({ ...input, location: session.location })
  98. yield* sessions.switchModel(input)
  99. }),
  100. interrupt: sessions.interrupt,
  101. prompt: (input) =>
  102. sessions.prompt({
  103. id: input.id,
  104. sessionID: input.sessionID,
  105. prompt: input.prompt,
  106. delivery: input.delivery,
  107. }),
  108. messages: (input) =>
  109. sessions.messages({
  110. sessionID: input.sessionID,
  111. limit: input.limit,
  112. order: input.order,
  113. cursor: input.cursor,
  114. }),
  115. message: (input) => sessions.message({ sessionID: input.sessionID, messageID: input.messageID }),
  116. context: sessions.context,
  117. events: (input) => sessions.events({ sessionID: input.sessionID, after: input.after }),
  118. },
  119. })
  120. }),
  121. ).pipe(Layer.provide(Layer.merge(ApplicationToolsLayer, SessionsLayer)))
  122. // TODO: Add OpenCode.create(...) as the Promise facade over the same native API semantics.