agent.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. export * as AgentV2 from "./agent"
  2. import { Array, Context, Effect, Layer, Schema, Scope } from "effect"
  3. import { castDraft, enableMapSet, type Draft } from "immer"
  4. import { ModelV2 } from "./model"
  5. import { PermissionSchema } from "./permission/schema"
  6. import { ProviderV2 } from "./provider"
  7. import { PositiveInt } from "./schema"
  8. import { State } from "./state"
  9. export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
  10. export type ID = typeof ID.Type
  11. export const defaultID = ID.make("build")
  12. export const Color = Schema.Union([
  13. Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
  14. Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
  15. ])
  16. export class Info extends Schema.Class<Info>("AgentV2.Info")({
  17. id: ID,
  18. model: ModelV2.Ref.pipe(Schema.optional),
  19. request: ProviderV2.Request,
  20. system: Schema.String.pipe(Schema.optional),
  21. description: Schema.String.pipe(Schema.optional),
  22. mode: Schema.Literals(["subagent", "primary", "all"]),
  23. hidden: Schema.Boolean,
  24. color: Color.pipe(Schema.optional),
  25. steps: PositiveInt.pipe(Schema.optional),
  26. permissions: PermissionSchema.Ruleset,
  27. }) {
  28. static empty(id: ID) {
  29. return new Info({
  30. id,
  31. request: {
  32. headers: {},
  33. body: {},
  34. },
  35. mode: "all",
  36. hidden: false,
  37. permissions: [],
  38. })
  39. }
  40. }
  41. export interface Selection {
  42. readonly id: ID
  43. readonly info: Info | undefined
  44. }
  45. type Data = {
  46. agents: Map<ID, Info>
  47. default?: ID
  48. }
  49. export type Editor = {
  50. list: () => readonly Info[]
  51. get: (id: ID) => Info | undefined
  52. default: (id: ID | undefined) => void
  53. update: (id: ID, fn: (agent: Draft<Info>) => void) => void
  54. remove: (id: ID) => void
  55. }
  56. export interface Interface {
  57. readonly transform: State.Interface<Data, Editor>["transform"]
  58. readonly update: State.Interface<Data, Editor>["update"]
  59. readonly get: (id: ID) => Effect.Effect<Info | undefined>
  60. readonly default: () => Effect.Effect<Info | undefined>
  61. readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
  62. readonly select: (id?: ID | string) => Effect.Effect<Selection>
  63. readonly all: () => Effect.Effect<Info[]>
  64. }
  65. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
  66. enableMapSet()
  67. export const layer = Layer.effect(
  68. Service,
  69. Effect.gen(function* () {
  70. const state = State.create<Data, Editor>({
  71. initial: () => ({ agents: new Map() }),
  72. editor: (draft) => ({
  73. list: () => Array.fromIterable(draft.agents.values()) as Info[],
  74. get: (id) => draft.agents.get(id),
  75. default: (id) => {
  76. draft.default = id
  77. },
  78. update: (id, fn) => {
  79. const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
  80. if (!draft.agents.has(id)) draft.agents.set(id, current)
  81. fn(current)
  82. current.id = id
  83. },
  84. remove: (id) => {
  85. draft.agents.delete(id)
  86. },
  87. }),
  88. })
  89. const selectable = (agent: Info | undefined) =>
  90. agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
  91. const selectedDefault = () => {
  92. const data = state.get()
  93. const configured = data.default ? selectable(data.agents.get(data.default)) : undefined
  94. if (configured) return configured
  95. const build = selectable(data.agents.get(ID.make("build")))
  96. if (build) return build
  97. for (const agent of data.agents.values()) {
  98. const fallback = selectable(agent)
  99. if (fallback) return fallback
  100. }
  101. }
  102. return Service.of({
  103. transform: state.transform,
  104. update: state.update,
  105. get: Effect.fn("AgentV2.get")(function* (id) {
  106. return state.get().agents.get(id)
  107. }),
  108. default: Effect.fn("AgentV2.default")(function* () {
  109. return selectedDefault()
  110. }),
  111. resolve: Effect.fn("AgentV2.resolve")(function* (id) {
  112. if (id !== undefined) return state.get().agents.get(ID.make(id))
  113. return selectedDefault()
  114. }),
  115. select: Effect.fn("AgentV2.select")(function* (id) {
  116. if (id !== undefined) {
  117. const selected = ID.make(id)
  118. return { id: selected, info: state.get().agents.get(selected) }
  119. }
  120. const info = selectedDefault()
  121. return { id: info?.id ?? defaultID, info }
  122. }),
  123. all: Effect.fn("AgentV2.all")(function* () {
  124. return Array.fromIterable(state.get().agents.values())
  125. }),
  126. })
  127. }),
  128. )
  129. export const locationLayer = layer