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