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,185 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { InstructionContext } from "@opencode-ai/core/instruction-context"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context-registry"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(Layer.empty)
describe("InstructionContext", () => {
it.live("loads global and upward project AGENTS.md files as one aggregate context", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((tmp) =>
Effect.gen(function* () {
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
const directory = path.join(project, "packages", "core")
const outside = path.join(tmp.path, "AGENTS.md")
const globalFile = path.join(global, "AGENTS.md")
const projectFile = path.join(project, "AGENTS.md")
const packageFile = path.join(directory, "AGENTS.md")
yield* Effect.promise(async () => {
await fs.mkdir(global, { recursive: true })
await fs.mkdir(directory, { recursive: true })
await fs.writeFile(outside, "outside")
await fs.writeFile(globalFile, "global")
await fs.writeFile(projectFile, "project")
await fs.writeFile(packageFile, "package")
})
const load = SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(FSUtil.defaultLayer),
Effect.provide(Global.layerWith({ config: global })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(project) },
),
),
),
),
)
const initialized = yield* SystemContext.initialize(yield* load)
expect(initialized.baseline).toBe(
[
`Instructions from: ${globalFile}\nglobal`,
`Instructions from: ${packageFile}\npackage`,
`Instructions from: ${projectFile}\nproject`,
].join("\n\n"),
)
expect(initialized.baseline).not.toContain("outside")
yield* Effect.promise(() => fs.writeFile(packageFile, "changed"))
expect(yield* SystemContext.reconcile(yield* load, initialized.snapshot)).toMatchObject({
_tag: "Updated",
text: expect.stringContaining(`Instructions from: ${packageFile}\nchanged`),
})
yield* Effect.promise(() => Promise.all([fs.rm(globalFile), fs.rm(packageFile), fs.rm(projectFile)]))
expect(yield* SystemContext.reconcile(yield* load, initialized.snapshot)).toEqual({
_tag: "Updated",
text: "Previously loaded instructions no longer apply.",
snapshot: {},
})
}),
),
),
)
it.live("keeps an empty AGENTS.md as available context", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((tmp) =>
Effect.gen(function* () {
const file = path.join(tmp.path, "AGENTS.md")
yield* Effect.promise(() => fs.writeFile(file, ""))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(FSUtil.defaultLayer),
Effect.provide(Global.layerWith({ config: path.join(tmp.path, "global") })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })),
),
),
)
expect((yield* SystemContext.initialize(context)).baseline).toBe(`Instructions from: ${file}\n`)
}),
),
),
)
it.effect("preserves admitted instructions while observation is unavailable", () =>
Effect.gen(function* () {
const failingFS = Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.fail(new FSUtil.FileSystemError({ method: "up" })) })),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(failingFS),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
),
),
)
expect(
yield* SystemContext.reconcile(context, {
"core/instructions": {
value: [{ path: "/repo/AGENTS.md", content: "old" }],
removed: "Previously loaded instructions no longer apply.",
},
}),
).toEqual({ _tag: "Unchanged" })
}),
)
it.effect("preserves admitted instructions when a discovered file disappears before read", () =>
Effect.gen(function* () {
const file = AbsolutePath.make("/repo/AGENTS.md")
const racingFS = Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) =>
FSUtil.Service.of({
...fs,
up: () => Effect.succeed([file]),
readFileStringSafe: () => Effect.succeed(undefined),
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
const context = yield* SystemContextRegistry.Service.pipe(
Effect.flatMap((service) => service.load()),
Effect.provide(InstructionContext.layer.pipe(Layer.provideMerge(SystemContextRegistry.layer))),
Effect.provide(racingFS),
Effect.provide(Global.layerWith({ config: "/global" })),
Effect.provide(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
),
),
)
expect(
yield* SystemContext.reconcile(context, {
"core/instructions": {
value: [{ path: file, content: "old" }],
removed: "Previously loaded instructions no longer apply.",
},
}),
).toEqual({ _tag: "Unchanged" })
}),
)
})

View file

@ -19,8 +19,8 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionSystemContext } from "@opencode-ai/core/session-system-context"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context-registry"
import { describe, expect } from "bun:test"
import { eq } from "drizzle-orm"
import { Effect, Layer, Schema } from "effect"
@ -57,22 +57,25 @@ const model = OpenAIChat.route
})
.model({ id: "gpt-4o-mini" })
const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
const systemContext = Layer.succeed(
SessionSystemContext.Service,
SessionSystemContext.Service.of({
load: () =>
Effect.succeed(
SystemContext.make({
key: SystemContext.Key.make("test/context"),
codec: Schema.toCodecJson(Schema.String),
load: Effect.succeed("Recorded context"),
baseline: String,
update: (_previous, current) => current,
removed: () => "Recorded context removed",
}),
),
}),
)
const systemContext = Layer.effectDiscard(
SystemContextRegistry.Service.pipe(
Effect.flatMap((registry) =>
registry.contribute({
key: "test/context",
load: Effect.succeed(
SystemContext.make({
key: SystemContext.Key.make("test/context"),
codec: Schema.toCodecJson(Schema.String),
load: Effect.succeed("Recorded context"),
baseline: String,
update: (_previous, current) => current,
removed: () => "Recorded context removed",
}),
),
}),
),
),
).pipe(Layer.provideMerge(SystemContextRegistry.layer))
const runner = SessionRunnerLLM.defaultLayer.pipe(
Layer.provide(database),
Layer.provide(store),

View file

@ -34,8 +34,8 @@ import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { NativeTool } from "@opencode-ai/core/tool/native"
import { SessionContextEpochTable, SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionSystemContext } from "@opencode-ai/core/session-system-context"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context-registry"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Cause, DateTime, Deferred, Effect, Fiber, Layer, Schema, Stream } from "effect"
@ -145,28 +145,31 @@ const systemContextKey = SystemContext.Key.make("test/context")
let systemBaseline = "Initial context"
let systemRemoved = false
let systemUnavailable = false
const systemContext = Layer.succeed(
SessionSystemContext.Service,
SessionSystemContext.Service.of({
load: () =>
Effect.succeed(
SystemContext.combine(
systemRemoved
? []
: [
SystemContext.make({
key: systemContextKey,
codec: Schema.toCodecJson(Schema.String),
load: Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline)),
baseline: String,
update: (_previous, current) => current,
removed: () => "System context source removed: test/context",
}),
],
const systemContext = Layer.effectDiscard(
SystemContextRegistry.Service.pipe(
Effect.flatMap((registry) =>
registry.contribute({
key: "test/context",
load: Effect.sync(() =>
SystemContext.combine(
systemRemoved
? []
: [
SystemContext.make({
key: systemContextKey,
codec: Schema.toCodecJson(Schema.String),
load: Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline)),
baseline: String,
update: (_previous, current) => current,
removed: () => "System context source removed: test/context",
}),
],
),
),
),
}),
)
}),
),
),
).pipe(Layer.provideMerge(SystemContextRegistry.layer))
const runner = SessionRunnerLLM.layer.pipe(
Layer.provide(database),
Layer.provide(store),

View file

@ -2,9 +2,12 @@ import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import * as TestClock from "effect/testing/TestClock"
import { Location } from "@opencode-ai/core/location"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionSystemContext } from "@opencode-ai/core/session-system-context"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context-builtins"
import { SystemContextRegistry } from "@opencode-ai/core/system-context-registry"
import { location } from "./fixture/location"
import { testEffect } from "./lib/effect"
@ -12,24 +15,44 @@ const directory = AbsolutePath.make("/repo/packages/core")
const projectDirectory = AbsolutePath.make("/repo")
const timestamp = Date.parse("2026-06-03T12:00:00.000Z")
const localDate = (time: number) => new Date(time).toDateString()
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory }, { projectDirectory, vcs: { type: "git", store: AbsolutePath.make("/repo/.git") } }),
),
)
const it = testEffect(
SessionSystemContext.locationLayer.pipe(
Layer.provide(
Layer.succeed(
Location.Service,
Location.Service.of(
location({ directory }, { projectDirectory, vcs: { type: "git", store: AbsolutePath.make("/repo/.git") } }),
),
),
SystemContextBuiltIns.locationLayer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layerWith({ config: "/global" })),
Layer.provide(locationLayer),
),
)
const instructionFS = Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) =>
FSUtil.Service.of({
...fs,
up: () => Effect.succeed(["/repo/AGENTS.md"]),
readFileStringSafe: (path) => Effect.succeed(path === "/repo/AGENTS.md" ? "Be precise." : undefined),
}),
),
),
).pipe(Layer.provide(FSUtil.defaultLayer))
const itWithInstructions = testEffect(
SystemContextBuiltIns.locationLayer.pipe(
Layer.provide(instructionFS),
Layer.provide(Global.layerWith({ config: "/global" })),
Layer.provide(locationLayer),
),
)
describe("SessionSystemContext", () => {
describe("SystemContextBuiltIns", () => {
it.effect("loads location-scoped environment and host-local date context", () =>
Effect.gen(function* () {
yield* TestClock.setTime(timestamp)
const context = yield* SessionSystemContext.Service
const context = yield* SystemContextRegistry.Service
const initialized = yield* SystemContext.initialize(yield* context.load())
expect(initialized.baseline).toBe(
@ -51,7 +74,7 @@ describe("SessionSystemContext", () => {
it.effect("reconciles the date without repeating unchanged environment context", () =>
Effect.gen(function* () {
yield* TestClock.setTime(timestamp)
const context = yield* SessionSystemContext.Service
const context = yield* SystemContextRegistry.Service
const initialized = yield* SystemContext.initialize(yield* context.load())
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
@ -67,11 +90,34 @@ describe("SessionSystemContext", () => {
it.effect("does not update again within the same local calendar day", () =>
Effect.gen(function* () {
yield* TestClock.setTime(timestamp)
const context = yield* SessionSystemContext.Service
const context = yield* SystemContextRegistry.Service
const initialized = yield* SystemContext.initialize(yield* context.load())
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
expect(yield* SystemContext.reconcile(yield* context.load(), initialized.snapshot)).toEqual({ _tag: "Unchanged" })
}),
)
itWithInstructions.effect("composes ambient instructions after built-in context", () =>
Effect.gen(function* () {
yield* TestClock.setTime(timestamp)
const context = yield* SystemContextRegistry.Service
expect((yield* SystemContext.initialize(yield* context.load())).baseline).toBe(
[
"Here is some useful information about the environment you are running in:",
"<env>",
` Working directory: ${directory}`,
` Workspace root folder: ${projectDirectory}`,
" Is directory a git repo: yes",
` Platform: ${process.platform}`,
"</env>",
"",
`Today's date: ${localDate(timestamp)}`,
"",
"Instructions from: /repo/AGENTS.md\nBe precise.",
].join("\n"),
)
}),
)
})

View file

@ -0,0 +1,113 @@
import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Schema, Scope } from "effect"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context-registry"
import { testEffect } from "./lib/effect"
const contribution = (key: string, text: string, sourceKey = key) => ({
key,
load: Effect.succeed(
SystemContext.make({
key: SystemContext.Key.make(sourceKey),
codec: Schema.toCodecJson(Schema.String),
load: Effect.succeed(text),
baseline: String,
update: (_previous, current) => current,
}),
),
})
const it = testEffect(SystemContextRegistry.layer)
describe("SystemContextRegistry", () => {
it.effect("loads empty system context when there are no contributions", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
expect(yield* SystemContext.initialize(yield* registry.load())).toEqual({ baseline: "", snapshot: {} })
}),
)
it.effect("loads scoped contributions in stable key order", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
yield* registry.contribute(contribution("test/second", "second"))
yield* registry.contribute(contribution("test/first", "first"))
expect((yield* SystemContext.initialize(yield* registry.load())).baseline).toBe("first\n\nsecond")
}),
)
it.effect("re-evaluates contribution producers on each load", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
let loads = 0
yield* registry.contribute({
key: "test/dynamic",
load: Effect.sync(() => {
loads++
return SystemContext.empty
}),
})
yield* registry.load()
yield* registry.load()
expect(loads).toBe(2)
}),
)
it.effect("propagates contribution producer failures", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
const failure = new Error("contribution failed")
yield* registry.contribute({ key: "test/failure", load: Effect.die(failure) })
const exit = yield* registry.load().pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBe(failure)
}),
)
it.effect("rejects duplicate source keys from separate contributions", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
yield* registry.contribute(contribution("test/first", "first", "test/duplicate"))
yield* registry.contribute(contribution("test/second", "second", "test/duplicate"))
const exit = yield* registry.load().pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
expect(Cause.squash(exit.cause)).toBeInstanceOf(SystemContext.DuplicateKeyError)
expect(Cause.squash(exit.cause)).toMatchObject({ key: SystemContext.Key.make("test/duplicate") })
}
}),
)
it.effect("rejects duplicate contribution keys", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
yield* registry.contribute(contribution("test/duplicate", "first"))
const exit = yield* registry.contribute(contribution("test/duplicate", "second", "test/other")).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("Duplicate system context contribution key")
}),
)
it.effect("removes a contribution when its owning scope closes", () =>
Effect.gen(function* () {
const registry = yield* SystemContextRegistry.Service
const scope = yield* Scope.make()
yield* registry.contribute(contribution("test/scoped", "scoped")).pipe(Scope.provide(scope))
expect((yield* SystemContext.initialize(yield* registry.load())).baseline).toBe("scoped")
yield* Scope.close(scope, Exit.void)
expect(yield* SystemContext.initialize(yield* registry.load())).toEqual({ baseline: "", snapshot: {} })
}),
)
})