refactor(core): rename system context to instructions (#35583)
This commit is contained in:
parent
1a52e1118e
commit
91f1815732
62 changed files with 1482 additions and 1005 deletions
|
|
@ -15,6 +15,7 @@ import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migrat
|
|||
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
||||
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
||||
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
|
||||
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
|
@ -73,11 +74,11 @@ describe("DatabaseMigration", () => {
|
|||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`),
|
||||
).toEqual({ name: "session_input" })
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_context_epoch'`),
|
||||
).toEqual({ name: "session_context_epoch" })
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'instruction_checkpoint'`),
|
||||
).toEqual({ name: "instruction_checkpoint" })
|
||||
expect(
|
||||
yield* db.get(
|
||||
sql`SELECT name FROM pragma_table_info('session_context_epoch') WHERE name IN ('agent', 'replacement_seq', 'revision')`,
|
||||
sql`SELECT name FROM pragma_table_info('instruction_checkpoint') WHERE name IN ('agent', 'replacement_seq', 'revision')`,
|
||||
),
|
||||
).toBeUndefined()
|
||||
expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: migrations.length })
|
||||
|
|
@ -131,6 +132,42 @@ describe("DatabaseMigration", () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("renames instruction state without losing rows or durable updates", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_context_entry (session_id text NOT NULL, key text NOT NULL, value text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, PRIMARY KEY(session_id, key))`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_context_epoch (session_id text PRIMARY KEY, baseline text NOT NULL, snapshot text NOT NULL, baseline_seq integer NOT NULL)`,
|
||||
)
|
||||
yield* db.run(sql`CREATE TABLE event (type text NOT NULL)`)
|
||||
yield* db.run(sql`INSERT INTO session_context_entry VALUES ('ses_test', 'plan', '"ready"', 1, 2)`)
|
||||
yield* db.run(sql`INSERT INTO session_context_epoch VALUES ('ses_test', 'baseline', '{}', 7)`)
|
||||
yield* db.run(sql`INSERT INTO event VALUES ('session.context.updated.1')`)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [renameInstructionsMigration])
|
||||
|
||||
expect(yield* db.get(sql`SELECT * FROM instruction_entry`)).toEqual({
|
||||
session_id: "ses_test",
|
||||
key: "plan",
|
||||
value: '"ready"',
|
||||
time_created: 1,
|
||||
time_updated: 2,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT * FROM instruction_checkpoint`)).toEqual({
|
||||
session_id: "ses_test",
|
||||
baseline: "baseline",
|
||||
snapshot: "{}",
|
||||
baseline_seq: 7,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT type FROM event`)).toEqual({ type: "session.instructions.updated.1" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps legacy credential fields nullable", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -264,10 +301,12 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||
sql`INSERT INTO instruction_checkpoint (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||
)
|
||||
yield* db.run(sql`ALTER TABLE instruction_checkpoint RENAME TO session_context_epoch`)
|
||||
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
|
||||
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
|
||||
yield* db.run(sql`ALTER TABLE session_context_epoch RENAME TO instruction_checkpoint`)
|
||||
|
||||
const database = Layer.succeed(Database.Service, { db })
|
||||
yield* EventV2.Service.use((service) =>
|
||||
|
|
@ -299,7 +338,7 @@ describe("DatabaseMigration", () => {
|
|||
(SELECT COUNT(*) FROM workspace) AS workspaces,
|
||||
(SELECT COUNT(*) FROM session_input) AS sessionInputs,
|
||||
(SELECT COUNT(*) FROM session_message) AS sessionMessages,
|
||||
(SELECT COUNT(*) FROM session_context_epoch) AS contextEpochs,
|
||||
(SELECT COUNT(*) FROM instruction_checkpoint) AS instructionCheckpoints,
|
||||
(SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
|
||||
(SELECT type FROM event WHERE aggregate_id = 'session') AS eventType
|
||||
`),
|
||||
|
|
@ -311,7 +350,7 @@ describe("DatabaseMigration", () => {
|
|||
workspaces: 0,
|
||||
sessionInputs: 0,
|
||||
sessionMessages: 0,
|
||||
contextEpochs: 0,
|
||||
instructionCheckpoints: 0,
|
||||
seq: 0,
|
||||
eventType: "session.updated.1",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -712,8 +712,8 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = Session.ID.create()
|
||||
const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
|
||||
yield* events.project(SessionEvent.ContextUpdated, (event) =>
|
||||
const received = new Array<typeof SessionEvent.InstructionsUpdated.Type>()
|
||||
yield* events.project(SessionEvent.InstructionsUpdated, (event) =>
|
||||
Effect.sync(() => {
|
||||
received.push(event)
|
||||
}),
|
||||
|
|
@ -722,7 +722,7 @@ describe("EventV2", () => {
|
|||
yield* events.replay({
|
||||
id: EventV2.ID.create(),
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
|
||||
type: EventV2.versionedType(SessionEvent.InstructionsUpdated.type, 1),
|
||||
seq: 0,
|
||||
aggregateID,
|
||||
data: { sessionID: aggregateID, text: "context" },
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { location } from "./fixture/location"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
|
@ -21,13 +21,13 @@ const instructionLayer = (input: {
|
|||
locationServiceLayer: Layer.Layer<Location.Service>
|
||||
filesystemLayer?: Layer.Layer<FSUtil.Service>
|
||||
}) =>
|
||||
AppNodeBuilder.build(InstructionContext.node, [
|
||||
AppNodeBuilder.build(InstructionDiscovery.node, [
|
||||
[Global.node, Global.layerWith({ config: input.config })],
|
||||
[Location.node, input.locationServiceLayer],
|
||||
...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []),
|
||||
])
|
||||
|
||||
describe("InstructionContext", () => {
|
||||
describe("InstructionDiscovery", () => {
|
||||
it.live("loads global and upward project AGENTS.md files as one aggregate context", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
@ -51,7 +51,7 @@ describe("InstructionContext", () => {
|
|||
await fs.writeFile(packageFile, "package")
|
||||
})
|
||||
|
||||
const load = InstructionContext.Service.pipe(
|
||||
const load = InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -69,7 +69,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const initialized = yield* SystemContext.initialize(yield* load)
|
||||
const initialized = yield* Instructions.initialize(yield* load)
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
`Instructions from: ${globalFile}\nglobal`,
|
||||
|
|
@ -80,13 +80,13 @@ describe("InstructionContext", () => {
|
|||
expect(initialized.text).not.toContain("outside")
|
||||
|
||||
yield* Effect.promise(() => fs.writeFile(packageFile, "changed"))
|
||||
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toMatchObject({
|
||||
expect(yield* Instructions.reconcile(yield* load, initialized.applied)).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: expect.stringContaining(`Instructions from: ${packageFile}\nchanged`),
|
||||
})
|
||||
|
||||
yield* Effect.promise(() => fs.rm(packageFile))
|
||||
const partial = yield* SystemContext.reconcile(yield* load, initialized.applied)
|
||||
const partial = yield* Instructions.reconcile(yield* load, initialized.applied)
|
||||
expect(partial).toEqual({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -98,7 +98,7 @@ describe("InstructionContext", () => {
|
|||
})
|
||||
|
||||
yield* Effect.promise(() => Promise.all([fs.rm(globalFile), fs.rm(projectFile)]))
|
||||
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toEqual({
|
||||
expect(yield* Instructions.reconcile(yield* load, initialized.applied)).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Previously loaded instructions no longer apply.",
|
||||
applied: {},
|
||||
|
|
@ -117,7 +117,7 @@ describe("InstructionContext", () => {
|
|||
Effect.gen(function* () {
|
||||
const file = path.join(tmp.path, "AGENTS.md")
|
||||
yield* Effect.promise(() => fs.writeFile(file, ""))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -130,7 +130,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
)
|
||||
|
||||
expect((yield* SystemContext.initialize(context)).text).toBe(`Instructions from: ${file}\n`)
|
||||
expect((yield* Instructions.initialize(context)).text).toBe(`Instructions from: ${file}\n`)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -146,7 +146,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -161,7 +161,7 @@ describe("InstructionContext", () => {
|
|||
)
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/instructions": {
|
||||
value: [{ path: "/repo/AGENTS.md", content: "old" }],
|
||||
removed: "Previously loaded instructions no longer apply.",
|
||||
|
|
@ -186,7 +186,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -201,7 +201,7 @@ describe("InstructionContext", () => {
|
|||
)
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/instructions": {
|
||||
value: [{ path: file, content: "old" }],
|
||||
removed: "Previously loaded instructions no longer apply.",
|
||||
|
|
@ -230,7 +230,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -260,7 +260,7 @@ describe("InstructionContext", () => {
|
|||
let scanned = false
|
||||
process.env.OPENCODE_DISABLE_PROJECT_CONFIG = "1"
|
||||
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -292,7 +292,7 @@ describe("InstructionContext", () => {
|
|||
it.effect("does not discover project instructions outside the canonical project root", () =>
|
||||
Effect.gen(function* () {
|
||||
let scanned = false
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
83
packages/core/test/instructions/builtins.test.ts
Normal file
83
packages/core/test/instructions/builtins.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
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 { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
|
||||
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/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(FSUtil.resolve("/repo/.git")) } },
|
||||
),
|
||||
),
|
||||
)
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(InstructionBuiltIns.node, [
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
|
||||
describe("InstructionBuiltIns", () => {
|
||||
it.effect("loads location-scoped environment and host-local date instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
expect(initialized.text).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)}`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles the date without repeating unchanged environment instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
|
||||
const refreshed = yield* Instructions.reconcile(yield* context.load(), initialized.applied)
|
||||
|
||||
expect(refreshed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not update again within the same local calendar day", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
|
||||
expect(yield* Instructions.reconcile(yield* context.load(), initialized.applied)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const key = SystemContext.Key.make
|
||||
const key = Instructions.Key.make
|
||||
const stringContext = (input: {
|
||||
key: string
|
||||
value: string | SystemContext.Unavailable
|
||||
value: string | Instructions.Unavailable
|
||||
baseline?: (value: string) => string
|
||||
update?: (previous: string, current: string) => string
|
||||
removed?: (value: string) => string
|
||||
}) =>
|
||||
SystemContext.make({
|
||||
Instructions.make({
|
||||
key: key(input.key),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.succeed(input.value),
|
||||
|
|
@ -20,10 +20,10 @@ const stringContext = (input: {
|
|||
removed: input.removed,
|
||||
})
|
||||
|
||||
describe("SystemContext", () => {
|
||||
describe("Instructions", () => {
|
||||
it.effect("stores the canonical JSON encoding of the loaded value", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.make({
|
||||
const context = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.DateFromString),
|
||||
load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
|
||||
|
|
@ -32,15 +32,15 @@ describe("SystemContext", () => {
|
|||
removed: () => "Date removed",
|
||||
})
|
||||
|
||||
expect((yield* SystemContext.initialize(context)).applied["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
|
||||
expect((yield* Instructions.initialize(context)).applied["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("loads once and initializes a baseline with the applied values", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = SystemContext.combine([
|
||||
SystemContext.make({
|
||||
const context = Instructions.combine([
|
||||
Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
|
|
@ -54,7 +54,7 @@ describe("SystemContext", () => {
|
|||
stringContext({ key: "core/location", value: "/repo", baseline: (value) => `Directory: ${value}` }),
|
||||
])
|
||||
|
||||
expect(yield* SystemContext.initialize(context)).toEqual({
|
||||
expect(yield* Instructions.initialize(context)).toEqual({
|
||||
text: "Today's date is 2026-06-03.\n\nDirectory: /repo",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
|
|
@ -71,7 +71,7 @@ describe("SystemContext", () => {
|
|||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
||||
}
|
||||
const changed = SystemContext.combine([
|
||||
const changed = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
|
|
@ -81,7 +81,7 @@ describe("SystemContext", () => {
|
|||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
])
|
||||
|
||||
expect(yield* SystemContext.reconcile(changed, previous)).toEqual({
|
||||
expect(yield* Instructions.reconcile(changed, previous)).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "The date changed from 2026-06-03 to 2026-06-04.",
|
||||
applied: {
|
||||
|
|
@ -91,8 +91,8 @@ describe("SystemContext", () => {
|
|||
})
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(
|
||||
SystemContext.combine([
|
||||
yield* Instructions.reconcile(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-03", removed: () => "The date was removed." }),
|
||||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
]),
|
||||
|
|
@ -110,7 +110,7 @@ describe("SystemContext", () => {
|
|||
baseline: (skill) => `Available skill: ${skill}`,
|
||||
})
|
||||
|
||||
expect(yield* SystemContext.reconcile(context, {})).toEqual({
|
||||
expect(yield* Instructions.reconcile(context, {})).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Available skill: effect",
|
||||
applied: { "core/skills": { value: "effect" } },
|
||||
|
|
@ -121,30 +121,28 @@ describe("SystemContext", () => {
|
|||
it.effect("retains the belief while a source is temporarily unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const previous = { "core/remote": { value: "instructions", removed: "Instructions removed" } }
|
||||
const context = stringContext({ key: "core/remote", value: SystemContext.unavailable })
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
expect(yield* SystemContext.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
|
||||
expect(yield* Instructions.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("blocks initialization while a source is unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* SystemContext.initialize(
|
||||
stringContext({ key: "core/remote", value: SystemContext.unavailable }),
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/remote", value: Instructions.unavailable }),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit))
|
||||
expect(Cause.squash(exit.cause)).toEqual(
|
||||
new SystemContext.InitializationBlocked({ keys: [key("core/remote")] }),
|
||||
)
|
||||
expect(Cause.squash(exit.cause)).toEqual(new Instructions.InitializationBlocked({ keys: [key("core/remote")] }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits the previously stored removal message", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(SystemContext.empty, {
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/instructions": { value: "contents", removed: "Instructions removed; stop applying them." },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -157,13 +155,13 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("retains an unannounced removal silently", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(yield* SystemContext.reconcile(SystemContext.empty, { "core/date": { value: "2026-06-04" } })).toEqual({
|
||||
expect(yield* Instructions.reconcile(Instructions.empty, { "core/date": { value: "2026-06-04" } })).toEqual({
|
||||
_tag: "Unchanged",
|
||||
})
|
||||
|
||||
// The retained belief survives alongside other updates.
|
||||
expect(
|
||||
yield* SystemContext.reconcile(stringContext({ key: "core/skills", value: "effect" }), {
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/skills", value: "effect" }), {
|
||||
"core/date": { value: "2026-06-04" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -180,7 +178,7 @@ describe("SystemContext", () => {
|
|||
it.effect("renders multiple removals in stable key order", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(SystemContext.empty, {
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/z": { value: "z", removed: "Removed z" },
|
||||
"core/a": { value: "a", removed: "Removed a" },
|
||||
}),
|
||||
|
|
@ -190,7 +188,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("rejects empty model-visible renderings", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* SystemContext.initialize(
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/empty", value: "value", baseline: () => "" }),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
|
|
@ -202,7 +200,7 @@ describe("SystemContext", () => {
|
|||
it.effect("re-announces the baseline when a stored value no longer decodes", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
|
||||
"core/date": { value: 42, removed: "Date removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -215,7 +213,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("renders undecodable re-announcements alongside other updates", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.combine([
|
||||
const context = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
|
|
@ -225,7 +223,7 @@ describe("SystemContext", () => {
|
|||
])
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/date": { value: "2026-06-03" },
|
||||
"core/location": { value: 42 },
|
||||
}),
|
||||
|
|
@ -243,7 +241,7 @@ describe("SystemContext", () => {
|
|||
it.effect("rebaselines from one coherent source observation", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = SystemContext.make({
|
||||
const context = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
|
|
@ -254,7 +252,7 @@ describe("SystemContext", () => {
|
|||
update: (_previous, current) => current,
|
||||
})
|
||||
|
||||
expect(yield* SystemContext.rebaseline(context, { "core/date": { value: "2026-06-03" } })).toEqual({
|
||||
expect(yield* Instructions.rebaseline(context, { "core/date": { value: "2026-06-03" } })).toEqual({
|
||||
text: "2026-06-04",
|
||||
applied: { "core/date": { value: "2026-06-04" } },
|
||||
})
|
||||
|
|
@ -264,17 +262,17 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("rebaselines an unavailable source from the last-applied belief", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.combine([
|
||||
const context = Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-04" }),
|
||||
stringContext({
|
||||
key: "core/remote",
|
||||
value: SystemContext.unavailable,
|
||||
value: Instructions.unavailable,
|
||||
baseline: (value) => `Instructions: ${value}`,
|
||||
}),
|
||||
])
|
||||
|
||||
expect(
|
||||
yield* SystemContext.rebaseline(context, {
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: "contents", removed: "Instructions removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -289,11 +287,11 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("drops undecodable beliefs and removed sources at rebaseline", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = stringContext({ key: "core/remote", value: SystemContext.unavailable })
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
// Undecodable belief cannot be restated; removed source entries self-clean.
|
||||
expect(
|
||||
yield* SystemContext.rebaseline(context, {
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: 42 },
|
||||
"core/gone": { value: "gone" },
|
||||
}),
|
||||
|
|
@ -315,7 +313,7 @@ describe("SystemContext", () => {
|
|||
]
|
||||
|
||||
expect(
|
||||
SystemContext.diffByKey(
|
||||
Instructions.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(value) => value.name,
|
||||
|
|
@ -337,19 +335,19 @@ describe("SystemContext", () => {
|
|||
it.effect("rejects duplicate source keys", () =>
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
SystemContext.combine([
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "one" }),
|
||||
stringContext({ key: "core/date", value: "two" }),
|
||||
]),
|
||||
).toThrow(new SystemContext.DuplicateKeyError({ key: key("core/date") }))
|
||||
).toThrow(new Instructions.DuplicateKeyError({ key: key("core/date") }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("combines contexts in order", () =>
|
||||
it.effect("combines instructions in order", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
(yield* SystemContext.initialize(
|
||||
SystemContext.combine([
|
||||
(yield* Instructions.initialize(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "date" }),
|
||||
stringContext({ key: "core/location", value: "location" }),
|
||||
]),
|
||||
|
|
@ -360,7 +358,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("requires namespaced source keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeKey = Schema.decodeUnknownSync(SystemContext.Key)
|
||||
const decodeKey = Schema.decodeUnknownSync(Instructions.Key)
|
||||
|
||||
expect(decodeKey("core/date")).toBe(key("core/date"))
|
||||
expect(() => decodeKey("date")).toThrow()
|
||||
|
|
@ -369,7 +367,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("requires namespaced applied keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeApplied = Schema.decodeUnknownSync(SystemContext.Applied)
|
||||
const decodeApplied = Schema.decodeUnknownSync(Instructions.Applied)
|
||||
|
||||
expect(Object.keys(decodeApplied({ "core/date": { value: "date" } }))).toEqual(["core/date"])
|
||||
expect(() => decodeApplied({ date: { value: "date" } })).toThrow()
|
||||
|
|
@ -4,17 +4,17 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
const guidanceLayer = (referenceLayer: Layer.Layer<Reference.Service>) =>
|
||||
AppNodeBuilder.build(ReferenceGuidance.node, [[Reference.node, referenceLayer]])
|
||||
|
||||
describe("ReferenceGuidance", () => {
|
||||
it.effect("lists available references in the system context", () =>
|
||||
it.effect("lists available references in the instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
|
||||
expect(generation.text).toContain("<available_references>")
|
||||
expect(generation.text).toContain("<name>docs</name>")
|
||||
|
|
@ -46,7 +46,7 @@ describe("ReferenceGuidance", () => {
|
|||
it.effect("omits guidance when no references are available", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
expect(generation.text).toBe("")
|
||||
}).pipe(Effect.provide(guidanceLayer(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })))),
|
||||
)
|
||||
|
|
@ -54,7 +54,7 @@ describe("ReferenceGuidance", () => {
|
|||
it.effect("omits references without descriptions", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
expect(generation.text).toBe("")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
|
|
@ -85,10 +85,10 @@ describe("ReferenceGuidance", () => {
|
|||
let references = [reference("docs", "Use for product documentation")]
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const initialized = yield* Instructions.initialize(yield* guidance.load())
|
||||
|
||||
references = [reference("docs", "Use for product documentation"), reference("examples", "Use for examples")]
|
||||
const added = yield* SystemContext.reconcile(yield* guidance.load(), initialized.applied)
|
||||
const added = yield* Instructions.reconcile(yield* guidance.load(), initialized.applied)
|
||||
expect(added).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -103,7 +103,7 @@ describe("ReferenceGuidance", () => {
|
|||
|
||||
references = [reference("examples", "Use for examples")]
|
||||
expect(
|
||||
yield* SystemContext.reconcile(yield* guidance.load(), added._tag === "Updated" ? added.applied : {}),
|
||||
yield* Instructions.reconcile(yield* guidance.load(), added._tag === "Updated" ? added.applied : {}),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: "The following project references are no longer available and must not be used: docs.",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import {
|
||||
SessionContextCheckpointTable,
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
|
|
@ -80,7 +80,7 @@ describe("SessionProjector", () => {
|
|||
])
|
||||
.run()
|
||||
yield* db
|
||||
.insert(SessionContextCheckpointTable)
|
||||
.insert(InstructionCheckpointTable)
|
||||
.values({ session_id: sessionID, baseline: "baseline", snapshot: {}, baseline_seq: 0 })
|
||||
.run()
|
||||
const events = yield* EventV2.Service
|
||||
|
|
@ -107,7 +107,7 @@ describe("SessionProjector", () => {
|
|||
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
|
||||
).toEqual([earlier])
|
||||
// A committed revert resets the context checkpoint so the next turn re-initializes.
|
||||
expect(yield* db.select().from(SessionContextCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||
expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
|||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
|
|
@ -73,18 +73,18 @@ const model = OpenAIChat.route
|
|||
})
|
||||
.model({ id: "gpt-4o-mini" })
|
||||
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
|
||||
const systemContext = Layer.mock(SystemContextBuiltIns.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[LayerNodePlatform.llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -119,8 +119,8 @@ const it = testEffect(
|
|||
AgentV2.node,
|
||||
ToolRegistry.node,
|
||||
SessionRunnerModel.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
Config.node,
|
||||
|
|
@ -133,8 +133,8 @@ const it = testEffect(
|
|||
[PermissionV2.node, permission],
|
||||
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@ import { Config } from "@opencode-ai/core/config"
|
|||
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||
import {
|
||||
SessionContextCheckpointTable,
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
|
|
@ -180,24 +180,24 @@ const models = SessionRunnerModel.layerWith((session) =>
|
|||
),
|
||||
),
|
||||
)
|
||||
const systemContextKey = SystemContext.Key.make("test/context")
|
||||
const systemContextKey = Instructions.Key.make("test/context")
|
||||
let systemBaseline = "Initial context"
|
||||
let systemRemoved = false
|
||||
let systemUnavailable = false
|
||||
let systemLoadHook = Effect.void
|
||||
const skillBaselines = new Map<AgentV2.ID, string>()
|
||||
const systemContext = Layer.mock(SystemContextBuiltIns.Service, {
|
||||
const systemContext = Layer.mock(InstructionBuiltIns.Service, {
|
||||
load: () =>
|
||||
Effect.sync(() =>
|
||||
SystemContext.combine(
|
||||
Instructions.combine(
|
||||
systemRemoved
|
||||
? []
|
||||
: [
|
||||
SystemContext.make({
|
||||
Instructions.make({
|
||||
key: systemContextKey,
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: systemLoadHook.pipe(
|
||||
Effect.andThen(Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline))),
|
||||
Effect.andThen(Effect.sync(() => (systemUnavailable ? Instructions.unavailable : systemBaseline))),
|
||||
),
|
||||
baseline: String,
|
||||
update: (_previous, current) => current,
|
||||
|
|
@ -207,24 +207,24 @@ const systemContext = Layer.mock(SystemContextBuiltIns.Service, {
|
|||
),
|
||||
),
|
||||
})
|
||||
const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
||||
load: (agent) =>
|
||||
Effect.succeed(
|
||||
skillBaselines.has(agent.id)
|
||||
? SystemContext.make({
|
||||
key: SystemContext.Key.make("test/skill-guidance"),
|
||||
? Instructions.make({
|
||||
key: Instructions.Key.make("test/skill-guidance"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.succeed(skillBaselines.get(agent.id)!),
|
||||
baseline: String,
|
||||
update: (_previous, current) => current,
|
||||
removed: () => "Skill guidance removed",
|
||||
})
|
||||
: SystemContext.empty,
|
||||
: Instructions.empty,
|
||||
),
|
||||
})
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
|
|
@ -246,8 +246,8 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
|||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[LayerNodePlatform.llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -285,9 +285,9 @@ const it = testEffect(
|
|||
ToolRegistry.toolsNode,
|
||||
echoNode,
|
||||
SessionRunnerModel.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
SessionContextEntry.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
InstructionEntry.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
Config.node,
|
||||
|
|
@ -300,8 +300,8 @@ const it = testEffect(
|
|||
[LayerNodePlatform.llmClient, client],
|
||||
[PermissionV2.node, permission],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -709,14 +709,14 @@ describe("SessionRunnerLLM", () => {
|
|||
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(SystemContext.InitializationBlocked)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Instructions.InitializationBlocked)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "steer")).toBe(true)
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get(),
|
||||
).toBeUndefined()
|
||||
|
||||
|
|
@ -747,8 +747,8 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get(),
|
||||
).toBeUndefined()
|
||||
|
||||
|
|
@ -774,16 +774,16 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const parent = yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
expect(parent).toBeDefined()
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, forked.id))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, forked.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie),
|
||||
).toEqual({ ...parent!, session_id: forked.id })
|
||||
|
|
@ -799,9 +799,9 @@ describe("SessionRunnerLLM", () => {
|
|||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
yield* db
|
||||
.update(SessionContextCheckpointTable)
|
||||
.update(InstructionCheckpointTable)
|
||||
.set({ snapshot: { invalid: { value: "bad" } } })
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
|
|
@ -815,9 +815,9 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
|
||||
expect(requests[0]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Initial context" }])
|
||||
const healed = yield* db
|
||||
.select({ snapshot: SessionContextCheckpointTable.snapshot })
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.select({ snapshot: InstructionCheckpointTable.snapshot })
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
expect(healed?.snapshot).toEqual({ "test/context": { value: "Initial context", removed: expect.any(String) } })
|
||||
|
|
@ -849,7 +849,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* db
|
||||
.select({ id: EventTable.id })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.type, "session.context.updated.1"))
|
||||
.where(eq(EventTable.type, "session.instructions.updated.1"))
|
||||
.all()
|
||||
.pipe(Effect.orDie),
|
||||
).toHaveLength(1)
|
||||
|
|
@ -1109,7 +1109,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
const contextEntries = yield* InstructionEntry.Service
|
||||
yield* contextEntries.put({ sessionID, key: "deploy-target", value: "production" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
|||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
|
|
@ -71,7 +71,7 @@ describe("SkillGuidance", () => {
|
|||
expect(
|
||||
yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied))),
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: "The following skills are no longer available and must not be used: effect.",
|
||||
|
|
@ -92,12 +92,12 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
skills = [effect, debugging]
|
||||
const added = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied)))
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied)))
|
||||
expect(added).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -113,7 +113,7 @@ describe("SkillGuidance", () => {
|
|||
const removed = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(
|
||||
Effect.flatMap((context) => SystemContext.reconcile(context, added._tag === "Updated" ? added.applied : {})),
|
||||
Effect.flatMap((context) => Instructions.reconcile(context, added._tag === "Updated" ? added.applied : {})),
|
||||
)
|
||||
expect(removed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
|
|
@ -129,13 +129,13 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
skills = [SkillV2.Info.make({ ...effect, description: "Build applications with Effect v4" })]
|
||||
expect(
|
||||
yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied))),
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: expect.stringContaining(
|
||||
|
|
@ -152,12 +152,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
||||
|
|
@ -171,12 +171,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ describe("SkillGuidance", () => {
|
|||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize))).text,
|
||||
(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).text,
|
||||
).toContain("<name>effect</name>")
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
|
@ -207,12 +207,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
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 { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
|
||||
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/repo"))
|
||||
const instructionFile = FSUtil.resolve("/repo/AGENTS.md")
|
||||
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(FSUtil.resolve("/repo/.git")) } },
|
||||
),
|
||||
),
|
||||
)
|
||||
const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, InstructionContext.node])
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(builtInsNode, [
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
const instructionFS = Layer.effect(
|
||||
FSUtil.Service,
|
||||
FSUtil.Service.pipe(
|
||||
Effect.map((fs) =>
|
||||
FSUtil.Service.of({
|
||||
...fs,
|
||||
up: () => Effect.succeed([instructionFile]),
|
||||
readFileStringSafe: (path) => Effect.succeed(path === instructionFile ? "Be precise." : undefined),
|
||||
}),
|
||||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const itWithInstructions = testEffect(
|
||||
AppNodeBuilder.build(builtInsNode, [
|
||||
[Location.node, locationLayer],
|
||||
[FSUtil.node, instructionFS],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
|
||||
describe("SystemContextBuiltIns", () => {
|
||||
it.effect("loads location-scoped environment and host-local date context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
expect(initialized.text).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)}`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles the date without repeating unchanged environment context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
|
||||
const refreshed = yield* SystemContext.reconcile(yield* context.load(), initialized.applied)
|
||||
|
||||
expect(refreshed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not update again within the same local calendar day", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
|
||||
expect(yield* SystemContext.reconcile(yield* context.load(), initialized.applied)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
|
||||
itWithInstructions.effect("composes ambient instructions after built-in context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const builtIns = yield* SystemContextBuiltIns.Service
|
||||
const instructions = yield* InstructionContext.Service
|
||||
const context = {
|
||||
load: () => Effect.all([builtIns.load(), instructions.load()]).pipe(Effect.map(SystemContext.combine)),
|
||||
}
|
||||
|
||||
expect((yield* SystemContext.initialize(yield* context.load())).text).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: ${instructionFile}\nBe precise.`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue