Skip to content

Commit 9583e08

Browse files
authored
feat(core): add location-scoped config loading (#29625)
1 parent 5fb85a6 commit 9583e08

89 files changed

Lines changed: 3509 additions & 527 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"glob": "13.0.5",
6464
"google-auth-library": "10.5.0",
6565
"immer": "11.1.4",
66+
"jsonc-parser": "3.3.1",
6667
"mime-types": "3.0.2",
6768
"minimatch": "10.2.5",
6869
"npm-package-arg": "13.0.2",

packages/core/src/agent.ts

Lines changed: 74 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,104 @@
11
export * as AgentV2 from "./agent"
22

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"
55
import { ModelV2 } from "./model"
66
import { PermissionV2 } from "./permission"
7-
import { PluginV2 } from "./plugin"
87
import { ProviderV2 } from "./provider"
8+
import { PositiveInt } from "./schema"
9+
import { State } from "./state"
910

1011
export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
1112
export type ID = typeof ID.Type
1213

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+
])
1518

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,
2321
model: ModelV2.Ref.pipe(Schema.optional),
22+
options: ProviderV2.Options,
2423
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+
}
3348

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+
}
3852

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+
}
4059

4160
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[]>
4965
}
5066

5167
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
5268

69+
enableMapSet()
70+
5371
export const layer = Layer.effect(
5472
Service,
5573
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+
},
7288
}),
89+
})
7390

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)
9996
}),
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())
10799
}),
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+
})
144101
}),
145102
)
146103

147-
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
104+
export const defaultLayer = layer

0 commit comments

Comments
 (0)