feat(core): register v2 system context sources

This commit is contained in:
Kit Langton 2026-06-04 20:28:48 -04:00
commit b28546a6a5
14 changed files with 588 additions and 107 deletions

View file

@ -0,0 +1,71 @@
export * as InstructionContext from "./instruction-context"
import { Array, Effect, Layer, Schema } from "effect"
import { join } from "path"
import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { Location } from "./location"
import { AbsolutePath } from "./schema"
import { SystemContext } from "./system-context"
import { SystemContextRegistry } from "./system-context-registry"
class File extends Schema.Class<File>("InstructionContext.File")({
path: AbsolutePath,
content: Schema.String,
}) {}
const Files = Schema.Array(File)
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const location = yield* Location.Service
const registry = yield* SystemContextRegistry.Service
const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
SystemContext.make({
key: SystemContext.Key.make("core/instructions"),
codec: Schema.toCodecJson(Files),
load: Effect.succeed(value),
baseline: render,
update: (_previous, current) => render(current),
removed: () => "Previously loaded instructions no longer apply.",
})
const observe = Effect.fn("InstructionContext.observe")(function* () {
const discovered = new Set(
(yield* fs.up({ targets: ["AGENTS.md"], start: location.directory, stop: location.project.directory })).map(
FSUtil.resolve,
),
)
const paths = Array.dedupe([FSUtil.resolve(join(global.config, "AGENTS.md")), ...discovered])
const files = yield* Effect.forEach(
paths,
(path) =>
fs.readFileStringSafe(path).pipe(
Effect.map((content) => (content === undefined ? undefined : new File({ path: AbsolutePath.make(path), content }))),
),
{ concurrency: "unbounded" },
)
if (files.some((file, index) => file === undefined && discovered.has(paths[index]))) return SystemContext.unavailable
return files.filter((file): file is File => file !== undefined)
})
yield* registry.contribute({
key: "core/instructions",
load: observe().pipe(
Effect.map((files) =>
files === SystemContext.unavailable ? source(files) : files.length === 0 ? SystemContext.empty : source(files),
),
Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))),
),
})
}),
)
export const locationLayer = layer
function render(files: ReadonlyArray<File>) {
return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
}

View file

@ -40,13 +40,14 @@ import { RequestExecutor } from "@opencode-ai/llm/route"
import * as SessionRunnerLLM from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model"
import { SessionRunCoordinator } from "./session/run-coordinator"
import { SessionSystemContext } from "./session-system-context"
import { SystemContextBuiltIns } from "./system-context-builtins"
import { FetchHttpClient } from "effect/unstable/http"
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
lookup: (ref: Location.Ref) => {
const location = Location.layer(ref)
const permissionsAndTools = ToolRegistry.layer.pipe(Layer.provideMerge(PermissionV2.locationLayer))
const systemContext = SystemContextBuiltIns.locationLayer
const services = Layer.mergeAll(
location,
Policy.locationLayer,
@ -56,12 +57,12 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
Catalog.locationLayer,
CommandV2.locationLayer,
AgentV2.locationLayer,
PluginBoot.locationLayer,
PluginBoot.locationLayer.pipe(Layer.provide(systemContext)),
FileSystem.locationLayer,
Watcher.locationLayer,
Pty.locationLayer,
SkillV2.locationLayer,
SessionSystemContext.locationLayer,
systemContext,
permissionsAndTools,
LocationMutation.locationLayer.pipe(Layer.orDie),
).pipe(Layer.provideMerge(location))

View file

@ -25,6 +25,7 @@ import { EnvPlugin } from "./env"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { SkillV2 } from "../skill"
import { SystemContextRegistry } from "../system-context-registry"
type Plugin = {
id: PluginV2.ID
@ -42,6 +43,7 @@ type Plugin = {
| Config.Service
| ModelsDev.Service
| SkillV2.Service
| SystemContextRegistry.Service
>
}
@ -67,6 +69,7 @@ export const layer = Layer.effect(
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const skill = yield* SkillV2.Service
const systemContext = yield* SystemContextRegistry.Service
const done = yield* Deferred.make<void>()
const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
@ -86,6 +89,7 @@ export const layer = Layer.effect(
Effect.provideService(Global.Service, global),
Effect.provideService(SkillV2.Service, skill),
Effect.provideService(PluginV2.Service, plugin),
Effect.provideService(SystemContextRegistry.Service, systemContext),
),
})
})

View file

@ -4,8 +4,8 @@ import { and, eq, isNull, lt, or, sql } from "drizzle-orm"
import { DateTime, Effect, Schema } from "effect"
import type { Database } from "../database/database"
import { EventV2 } from "../event"
import { SessionSystemContext } from "../session-system-context"
import { SystemContext } from "../system-context"
import { SystemContextRegistry } from "../system-context-registry"
import { SessionEvent } from "./event"
import { SessionMessageID } from "./message-id"
import { SessionSchema } from "./schema"
@ -16,7 +16,7 @@ type DatabaseService = Database.Interface["db"]
export const prepare = Effect.fn("SessionContextEpoch.prepare")(function* (
db: DatabaseService,
events: EventV2.Interface,
context: SessionSystemContext.Interface,
context: SystemContextRegistry.Interface,
sessionID: SessionSchema.ID,
) {
const [value, stored] = yield* Effect.all([context.load(), find(db, sessionID)], { concurrency: "unbounded" })

View file

@ -14,7 +14,7 @@ import { SessionRunnerModel } from "./model"
import { Database } from "../../database/database"
import { SessionInput } from "../input"
import { QuestionV2 } from "../../question"
import { SessionSystemContext } from "../../session-system-context"
import { SystemContextRegistry } from "../../system-context-registry"
import { SessionContextEpoch } from "../context-epoch"
/**
@ -36,8 +36,8 @@ import { SessionContextEpoch } from "../context-epoch"
* - [x] Resolve the selected model through the location-scoped runner environment.
* - [ ] Load the selected agent and effective permissions.
* - [ ] Build provider/model-specific base instructions and environment facts.
* - [ ] Load configured project instructions such as `AGENTS.md`, remote instructions, and
* nearby nested instructions discovered while files are read.
* - [x] Load global and upward project `AGENTS.md` instructions.
* - [ ] Load configured and remote instructions plus nearby nested instructions discovered while files are read.
* - [ ] List available skills in the system prompt and expose a tool for loading skill bodies.
* - [ ] Resolve referenced files, directories, agents, repositories, MCP resources, and media.
* - [ ] Apply steering reminders, plugin transforms, and structured-output policy.
@ -87,7 +87,7 @@ export const layer = Layer.effect(
const tools = yield* ToolRegistry.Service
const models = yield* SessionRunnerModel.Service
const store = yield* SessionStore.Service
const systemContext = yield* SessionSystemContext.Service
const systemContext = yield* SystemContextRegistry.Service
const db = (yield* Database.Service).db
const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) {
const session = yield* store.get(sessionID)

View file

@ -1,19 +1,15 @@
export * as SessionSystemContext from "./session-system-context"
export * as SystemContextBuiltIns from "./system-context-builtins"
import { Context, DateTime, Effect, Layer, Schema } from "effect"
import { DateTime, Effect, Layer, Schema } from "effect"
import { InstructionContext } from "./instruction-context"
import { Location } from "./location"
import { SystemContext } from "./system-context"
import { SystemContextRegistry } from "./system-context-registry"
export interface Interface {
readonly load: () => Effect.Effect<SystemContext.SystemContext>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionSystemContext") {}
export const layer = Layer.effect(
Service,
const builtIns = Layer.effectDiscard(
Effect.gen(function* () {
const location = yield* Location.Service
const registry = yield* SystemContextRegistry.Service
const environment = [
"<env>",
` Working directory: ${location.directory}`,
@ -40,8 +36,12 @@ export const layer = Layer.effect(
}),
])
return Service.of({ load: () => Effect.succeed(context) })
yield* registry.contribute({ key: "core/builtins", load: Effect.succeed(context) })
}),
)
export const layer = Layer.mergeAll(builtIns, InstructionContext.layer).pipe(
Layer.provideMerge(SystemContextRegistry.layer),
)
export const locationLayer = layer

View file

@ -0,0 +1,48 @@
export * as SystemContextRegistry from "./system-context-registry"
import { Context, Effect, Layer, Ref, Scope } from "effect"
import { SystemContext } from "./system-context"
export interface Contribution {
readonly key: string
readonly load: Effect.Effect<SystemContext.SystemContext>
}
export interface Interface {
readonly contribute: (contribution: Contribution) => Effect.Effect<void, never, Scope.Scope>
readonly load: () => Effect.Effect<SystemContext.SystemContext>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SystemContextRegistry") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const contributions = yield* Ref.make<ReadonlyArray<Contribution>>([])
return Service.of({
contribute: Effect.fn("SystemContextRegistry.contribute")(function* (contribution) {
yield* Effect.acquireRelease(
Ref.modify(contributions, (current) => {
if (current.some((item) => item.key === contribution.key)) return [false, current]
return [true, [...current, contribution]]
}).pipe(
Effect.flatMap((added) =>
added ? Effect.void : Effect.die(`Duplicate system context contribution key: ${contribution.key}`),
),
Effect.as(contribution),
),
(entry) => Ref.update(contributions, (current) => current.filter((item) => item !== entry)),
)
}),
load: Effect.fn("SystemContextRegistry.load")(function* () {
const current = (yield* Ref.get(contributions)).toSorted((a, b) => a.key.localeCompare(b.key))
return SystemContext.combine(
yield* Effect.forEach(current, (contribution) => contribution.load, { concurrency: "unbounded" }),
)
}),
})
}),
)
export const locationLayer = layer