agent.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. export * as AgentV2 from "./agent"
  2. import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array } from "effect"
  3. import { produce, type Draft } from "immer"
  4. import { ModelV2 } from "./model"
  5. import { PermissionV2 } from "./permission"
  6. import { PluginV2 } from "./plugin"
  7. import { ProviderV2 } from "./provider"
  8. export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
  9. export type ID = typeof ID.Type
  10. export const Mode = Schema.Literals(["subagent", "primary", "all"]).annotate({ identifier: "AgentV2.Mode" })
  11. export type Mode = typeof Mode.Type
  12. export const Info = Schema.Struct({
  13. name: ID,
  14. description: Schema.optional(Schema.String),
  15. mode: Mode,
  16. hidden: Schema.Boolean.pipe(Schema.optional),
  17. color: Schema.String.pipe(Schema.optional),
  18. permission: PermissionV2.Ruleset,
  19. model: ModelV2.Ref.pipe(Schema.optional),
  20. system: Schema.String.pipe(Schema.optional),
  21. options: ProviderV2.Options.pipe(Schema.optional),
  22. steps: Schema.Int.pipe(Schema.optional),
  23. }).annotate({ identifier: "AgentV2.Info" })
  24. export type Info = typeof Info.Type
  25. export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("AgentV2.NotFound", {
  26. agent: ID,
  27. }) {}
  28. export class InvalidDefaultError extends Schema.TaggedErrorClass<InvalidDefaultError>()("AgentV2.InvalidDefault", {
  29. agent: ID,
  30. reason: Schema.Literals(["missing", "subagent", "hidden"]),
  31. }) {}
  32. export class NoDefaultError extends Schema.TaggedErrorClass<NoDefaultError>()("AgentV2.NoDefault", {}) {}
  33. export interface Interface {
  34. readonly get: (agent: ID) => Effect.Effect<Info, NotFoundError>
  35. readonly list: () => Effect.Effect<Info[]>
  36. readonly update: (agent: ID, fn: (agent: Draft<Info>) => void) => Effect.Effect<void>
  37. readonly remove: (agent: ID) => Effect.Effect<void>
  38. readonly defaultInfo: () => Effect.Effect<Info, InvalidDefaultError | NoDefaultError>
  39. readonly defaultAgent: () => Effect.Effect<ID, InvalidDefaultError | NoDefaultError>
  40. readonly setDefault: (agent: ID) => Effect.Effect<void, NotFoundError>
  41. }
  42. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
  43. export const layer = Layer.effect(
  44. Service,
  45. Effect.gen(function* () {
  46. const plugin = yield* PluginV2.Service
  47. let agents = HashMap.empty<ID, Info>()
  48. let defaultAgent: ID | undefined
  49. const result: Interface = {
  50. get: Effect.fn("AgentV2.get")(function* (agent) {
  51. const match = HashMap.get(agents, agent)
  52. if (!match.valueOrUndefined) return yield* new NotFoundError({ agent })
  53. return match.value
  54. }),
  55. list: Effect.fn("AgentV2.list")(function* () {
  56. return pipe(
  57. HashMap.toValues(agents),
  58. Array.sortWith((agent) => agent.name, Order.String),
  59. )
  60. }),
  61. update: Effect.fnUntraced(function* (agent, fn) {
  62. const next = produce(
  63. HashMap.get(agents, agent).pipe(
  64. Option.getOrElse(
  65. () =>
  66. ({
  67. name: agent,
  68. mode: "all",
  69. permission: [],
  70. options: {
  71. headers: {},
  72. body: {},
  73. aisdk: {
  74. provider: {},
  75. request: {},
  76. },
  77. },
  78. }) satisfies Info,
  79. ),
  80. ),
  81. fn,
  82. )
  83. const updated = yield* plugin.trigger("agent.update", {}, { agent: next, cancel: false })
  84. if (updated.cancel) return
  85. agents = HashMap.set(agents, agent, { ...updated.agent, name: agent })
  86. }),
  87. remove: Effect.fn("AgentV2.remove")(function* (agent) {
  88. const existing = Option.getOrUndefined(HashMap.get(agents, agent))
  89. if (!existing) return
  90. if ((yield* plugin.trigger("agent.remove", { agent: existing }, { cancel: false })).cancel) return
  91. agents = HashMap.remove(agents, agent)
  92. if (defaultAgent === agent) defaultAgent = undefined
  93. }),
  94. defaultInfo: Effect.fn("AgentV2.defaultInfo")(function* () {
  95. const updated = yield* plugin.trigger("agent.default", {}, { agent: defaultAgent })
  96. const selected = updated.agent
  97. if (selected) {
  98. const agent = yield* result
  99. .get(selected)
  100. .pipe(
  101. Effect.catchTag("AgentV2.NotFound", () =>
  102. Effect.fail(new InvalidDefaultError({ agent: selected, reason: "missing" })),
  103. ),
  104. )
  105. if (agent.mode === "subagent") return yield* new InvalidDefaultError({ agent: selected, reason: "subagent" })
  106. if (agent.hidden === true) return yield* new InvalidDefaultError({ agent: selected, reason: "hidden" })
  107. return agent
  108. }
  109. const visible = pipe(
  110. yield* result.list(),
  111. Array.findFirst((agent) => agent.mode !== "subagent" && agent.hidden !== true),
  112. )
  113. if (Option.isSome(visible)) return visible.value
  114. return yield* new NoDefaultError()
  115. }),
  116. defaultAgent: Effect.fn("AgentV2.defaultAgent")(function* () {
  117. return (yield* result.defaultInfo()).name
  118. }),
  119. setDefault: Effect.fn("AgentV2.setDefault")(function* (agent) {
  120. yield* result.get(agent)
  121. defaultAgent = agent
  122. }),
  123. }
  124. return Service.of(result)
  125. }),
  126. )
  127. export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))