refactor(core): rename system context to instructions (#35583)

This commit is contained in:
Kit Langton 2026-07-06 14:29:29 -04:00 committed by GitHub
commit 91f1815732
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1482 additions and 1005 deletions

View file

@ -19,7 +19,7 @@
"./effect/layer-node": "./src/effect/layer-node.ts",
"./effect/app-node": "./src/effect/app-node.ts",
"./session/runner": "./src/session/runner/index.ts",
"./system-context": "./src/system-context/index.ts",
"./instructions": "./src/instructions/index.ts",
"./*": "./src/*.ts"
},
"imports": {

View file

@ -1,9 +1,9 @@
{
"version": "7",
"dialect": "sqlite",
"id": "96e9fe64-d810-4102-8f79-3317a88bb6d2",
"id": "992b24b9-f3e9-41f5-87a5-4917d1423169",
"prevIds": [
"22e57fed-b9b8-4e94-a3b4-f94bece680a8"
"96e9fe64-660f-4a73-9414-b38bb7eac290"
],
"ddl": [
{
@ -50,6 +50,14 @@
"name": "project",
"entityType": "tables"
},
{
"name": "instruction_checkpoint",
"entityType": "tables"
},
{
"name": "instruction_entry",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
@ -58,14 +66,6 @@
"name": "part",
"entityType": "tables"
},
{
"name": "session_context_epoch",
"entityType": "tables"
},
{
"name": "session_context_entry",
"entityType": "tables"
},
{
"name": "session_input",
"entityType": "tables"
@ -786,6 +786,96 @@
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "instruction_checkpoint"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "baseline",
"entityType": "columns",
"table": "instruction_checkpoint"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "snapshot",
"entityType": "columns",
"table": "instruction_checkpoint"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "baseline_seq",
"entityType": "columns",
"table": "instruction_checkpoint"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "instruction_entry"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "key",
"entityType": "columns",
"table": "instruction_entry"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "value",
"entityType": "columns",
"table": "instruction_entry"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "instruction_entry"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "instruction_entry"
},
{
"type": "text",
"notNull": false,
@ -896,96 +986,6 @@
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_context_epoch"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "baseline",
"entityType": "columns",
"table": "session_context_epoch"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "snapshot",
"entityType": "columns",
"table": "session_context_epoch"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "baseline_seq",
"entityType": "columns",
"table": "session_context_epoch"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_context_entry"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "key",
"entityType": "columns",
"table": "session_context_entry"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "value",
"entityType": "columns",
"table": "session_context_entry"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_context_entry"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_context_entry"
},
{
"type": "text",
"notNull": false,
@ -1621,6 +1621,36 @@
"entityType": "fks",
"table": "project_directory"
},
{
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_instruction_checkpoint_session_id_session_id_fk",
"entityType": "fks",
"table": "instruction_checkpoint"
},
{
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_instruction_entry_session_id_session_id_fk",
"entityType": "fks",
"table": "instruction_entry"
},
{
"columns": [
"session_id"
@ -1651,36 +1681,6 @@
"entityType": "fks",
"table": "part"
},
{
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_context_epoch_session_id_session_id_fk",
"entityType": "fks",
"table": "session_context_epoch"
},
{
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_context_entry_session_id_session_id_fk",
"entityType": "fks",
"table": "session_context_entry"
},
{
"columns": [
"session_id"
@ -1782,9 +1782,9 @@
"key"
],
"nameExplicit": false,
"name": "session_context_entry_pk",
"name": "instruction_entry_pk",
"entityType": "pks",
"table": "session_context_entry"
"table": "instruction_entry"
},
{
"columns": [
@ -1877,6 +1877,15 @@
"table": "project",
"entityType": "pks"
},
{
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "instruction_checkpoint_pk",
"table": "instruction_checkpoint",
"entityType": "pks"
},
{
"columns": [
"id"
@ -1895,15 +1904,6 @@
"table": "part",
"entityType": "pks"
},
{
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "session_context_epoch_pk",
"table": "session_context_epoch",
"entityType": "pks"
},
{
"columns": [
"id"
@ -2252,4 +2252,4 @@
}
],
"renames": []
}
}

View file

@ -44,5 +44,6 @@ export const migrations = (
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
import("./migration/20260703181610_event_created_column"),
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
import("./migration/20260705180000_rename_instructions"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]

View file

@ -0,0 +1,17 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260705180000_rename_instructions",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session_context_entry\` RENAME TO \`instruction_entry\``)
yield* tx.run(`ALTER TABLE \`session_context_epoch\` RENAME TO \`instruction_checkpoint\``)
yield* tx.run(`
UPDATE \`event\`
SET \`type\` = 'session.instructions.updated.1'
WHERE \`type\` = 'session.context.updated.1'
`)
})
},
} satisfies DatabaseMigration.Migration

View file

@ -125,6 +125,26 @@ export default {
\`commands\` text
);
`)
yield* tx.run(`
CREATE TABLE \`instruction_checkpoint\` (
\`session_id\` text PRIMARY KEY,
\`baseline\` text NOT NULL,
\`snapshot\` text NOT NULL,
\`baseline_seq\` integer NOT NULL,
CONSTRAINT \`fk_instruction_checkpoint_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`instruction_entry\` (
\`session_id\` text NOT NULL,
\`key\` text NOT NULL,
\`value\` text NOT NULL,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL,
CONSTRAINT \`instruction_entry_pk\` PRIMARY KEY(\`session_id\`, \`key\`),
CONSTRAINT \`fk_instruction_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`message\` (
\`id\` text PRIMARY KEY,
@ -146,26 +166,6 @@ export default {
CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_context_epoch\` (
\`session_id\` text PRIMARY KEY,
\`baseline\` text NOT NULL,
\`snapshot\` text NOT NULL,
\`baseline_seq\` integer NOT NULL,
CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
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,
CONSTRAINT \`session_context_entry_pk\` PRIMARY KEY(\`session_id\`, \`key\`),
CONSTRAINT \`fk_session_context_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_input\` (
\`id\` text PRIMARY KEY,

View file

@ -1,4 +1,4 @@
export * as InstructionContext from "./instruction-context"
export * as InstructionDiscovery from "./instruction-discovery"
import { Array, Context, Effect, Layer, Schema } from "effect"
import { isAbsolute, join, relative, sep } from "path"
@ -7,22 +7,22 @@ import { Flag } from "./flag/flag"
import { Global } from "./global"
import { Location } from "./location"
import { AbsolutePath } from "./schema"
import { SystemContext } from "./system-context/index"
import { Instructions } from "./instructions/index"
import { makeLocationNode } from "./effect/app-node"
class File extends Schema.Class<File>("InstructionContext.File")({
class File extends Schema.Class<File>("InstructionDiscovery.File")({
path: AbsolutePath,
content: Schema.String,
}) {}
const Files = Schema.Array(File)
const key = SystemContext.Key.make("core/instructions")
const key = Instructions.Key.make("core/instructions")
export interface Interface {
readonly load: () => Effect.Effect<SystemContext.SystemContext>
readonly load: () => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionContext") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionDiscovery") {}
const layer = Layer.effect(
Service,
@ -31,8 +31,8 @@ const layer = Layer.effect(
const global = yield* Global.Service
const location = yield* Location.Service
const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
SystemContext.make({
const source = (value: ReadonlyArray<File> | Instructions.Unavailable) =>
Instructions.make({
key,
codec: Schema.toCodecJson(Files),
load: Effect.succeed(value),
@ -42,7 +42,7 @@ const layer = Layer.effect(
removed: () => "Previously loaded instructions no longer apply.",
})
const observe = Effect.fn("InstructionContext.observe")(function* () {
const observe = Effect.fn("InstructionDiscovery.observe")(function* () {
const start = yield* fs.resolve(location.directory)
const stop = yield* fs.resolve(location.project.directory)
const fromProject = relative(stop, start)
@ -74,7 +74,7 @@ const layer = Layer.effect(
{ concurrency: "unbounded" },
)
if (files.some((file, index) => file === undefined && discovered.has(paths[index])))
return SystemContext.unavailable
return Instructions.unavailable
return files.filter((file): file is File => file !== undefined)
})
@ -82,14 +82,14 @@ const layer = Layer.effect(
load: () =>
observe().pipe(
Effect.map((files) =>
files === SystemContext.unavailable
files === Instructions.unavailable
? source(files)
: files.length === 0
? SystemContext.empty
? Instructions.empty
: source(files),
),
Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))),
Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))),
Effect.catch(() => Effect.succeed(source(Instructions.unavailable))),
Effect.catchDefect(() => Effect.succeed(source(Instructions.unavailable))),
),
})
}),

View file

@ -1,15 +1,15 @@
export * as SystemContextBuiltIns from "./builtins"
export * as InstructionBuiltIns from "./builtins"
import { makeLocationNode } from "../effect/app-node"
import { Context, DateTime, Effect, Layer, Schema } from "effect"
import { Location } from "../location"
import { SystemContext } from "./index"
import { Instructions } from "./index"
export interface Interface {
readonly load: () => Effect.Effect<SystemContext.SystemContext>
readonly load: () => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SystemContextBuiltIns") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionBuiltIns") {}
const layer = Layer.effect(
Service,
@ -23,17 +23,17 @@ const layer = Layer.effect(
` Platform: ${process.platform}`,
"</env>",
].join("\n")
const context = SystemContext.combine([
SystemContext.make({
key: SystemContext.Key.make("core/environment"),
const instructions = Instructions.combine([
Instructions.make({
key: Instructions.Key.make("core/environment"),
codec: Schema.toCodecJson(Schema.String),
load: Effect.succeed(environment),
baseline: (environment) =>
["Here is some useful information about the environment you are running in:", environment].join("\n"),
update: (_previous, environment) => ["The environment you are running in is now:", environment].join("\n"),
}),
SystemContext.make({
key: SystemContext.Key.make("core/date"),
Instructions.make({
key: Instructions.Key.make("core/date"),
codec: Schema.toCodecJson(Schema.String),
load: DateTime.nowAsDate.pipe(Effect.map((date) => date.toDateString())),
baseline: (date) => `Today's date: ${date}`,
@ -41,7 +41,7 @@ const layer = Layer.effect(
}),
])
return Service.of({ load: () => Effect.succeed(context) })
return Service.of({ load: () => Effect.succeed(instructions) })
}),
)

View file

@ -1,13 +1,13 @@
export * as SystemContext from "./index"
export * as Instructions from "./index"
import { Effect, Option, Schema } from "effect"
/**
* Models privileged system context as independently refreshable typed sources.
* Models privileged instructions as independently refreshable typed sources.
*
* `Source<A>` describes how to observe, compare, and render one value. `make`
* closes over `A`, producing an opaque `SystemContext` that composes uniformly
* with contexts built from other value types.
* closes over `A`, producing opaque `Instructions` that compose uniformly with
* instructions built from other value types.
*
* The durable `Applied` record tracks what the model was last told, per source:
* it is the model's current belief. Interpreters uphold one invariant
@ -16,21 +16,21 @@ import { Effect, Option, Schema } from "effect"
* baseline text.
*
* Returning `unavailable` means observation failed temporarily. It differs from
* removing a source from the context: the model's prior belief stands.
* removing a source from the instructions: the model's prior belief stands.
* `reconcile` retains the applied value silently, and `rebaseline` restates the
* belief by rendering the last-applied value instead of a live observation.
*
* @module
*/
/** Stable namespaced identity for one independently refreshable context source. */
/** Stable namespaced identity for one independently refreshable instruction source. */
export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/)).pipe(
Schema.brand("SystemContext.Key"),
Schema.brand("Instructions.Key"),
)
export type Key = typeof Key.Type
/** Indicates that a source could not be observed without treating it as removed. */
export const unavailable = Symbol.for("@opencode/SystemContext.Unavailable")
export const unavailable = Symbol.for("@opencode/Instructions.Unavailable")
export type Unavailable = typeof unavailable
/** Defines one typed source before its value type is hidden by `make`. */
@ -43,11 +43,11 @@ export interface Source<A> {
readonly removed?: (previous: A) => string
}
const ContextTypeId: unique symbol = Symbol.for("@opencode/SystemContext")
const InstructionsTypeId: unique symbol = Symbol.for("@opencode/Instructions")
/** Opaque carrier for composable system context sources. */
export interface SystemContext {
readonly [ContextTypeId]: ReadonlyArray<PackedSource>
/** Opaque carrier for composable instruction sources. */
export interface Instructions {
readonly [InstructionsTypeId]: ReadonlyArray<PackedSource>
}
/** The value last applied to the model for one admitted source. */
@ -76,19 +76,19 @@ export interface Updated {
export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated
export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()(
"SystemContext.InitializationBlocked",
"Instructions.InitializationBlocked",
{ keys: Schema.Array(Key) },
) {
override get message() {
return `System context initialization blocked by unavailable sources: ${this.keys.join(", ")}`
return `Instruction initialization blocked by unavailable sources: ${this.keys.join(", ")}`
}
}
export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("SystemContext.DuplicateKeyError", {
export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("Instructions.DuplicateKeyError", {
key: Key,
}) {
override get message() {
return `Duplicate system context key: ${this.key}`
return `Duplicate instruction key: ${this.key}`
}
}
@ -112,16 +112,16 @@ interface Entry {
readonly observed: Observed | Unavailable
}
/** The identity context. */
export const empty = context([])
/** The identity instruction set. */
export const empty = instructions([])
/** Closes a typed source into a context that composes with differently typed sources. */
export function make<A>(source: Source<A>): SystemContext {
/** Closes a typed source into instructions that compose with differently typed sources. */
export function make<A>(source: Source<A>): Instructions {
const decode = Schema.decodeUnknownOption(source.codec)
const encode = Schema.encodeSync(source.codec)
const equivalent = Schema.toEquivalence(source.codec)
const baseline = (value: A) => requireText(source.key, "baseline", source.baseline(value))
return context([
return instructions([
{
key: source.key,
recall: (stored) =>
@ -179,23 +179,23 @@ export function diffByKey<A>(
}
}
/** Combines contexts in order and rejects duplicate source keys immediately. */
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
const sources = values.flatMap((value) => value[ContextTypeId])
/** Combines instructions in order and rejects duplicate source keys immediately. */
export function combine(values: ReadonlyArray<Instructions>): Instructions {
const sources = values.flatMap((value) => value[InstructionsTypeId])
assertUniqueKeys(sources)
return context(sources)
return instructions(sources)
}
const observe = (value: SystemContext) =>
const observe = (value: Instructions) =>
Effect.forEach(
value[ContextTypeId],
value[InstructionsTypeId],
(source) =>
source.load.pipe(Effect.map((observed): Entry => ({ key: source.key, recall: source.recall, observed }))),
{ concurrency: "unbounded" },
)
/** Creates the first baseline. Blocks rather than admit a baseline missing an unobservable source. */
export function initialize(value: SystemContext): Effect.Effect<Baseline, InitializationBlocked> {
export function initialize(value: Instructions): Effect.Effect<Baseline, InitializationBlocked> {
return observe(value).pipe(
Effect.flatMap((entries) => {
const blocked = entries.flatMap((entry) => (entry.observed === unavailable ? [entry.key] : []))
@ -213,7 +213,7 @@ export function initialize(value: SystemContext): Effect.Effect<Baseline, Initia
}
/** Narrates drift between current source values and the model's beliefs. Never rewrites the baseline. */
export function reconcile(value: SystemContext, previous: Applied): Effect.Effect<ReconcileResult> {
export function reconcile(value: Instructions, previous: Applied): Effect.Effect<ReconcileResult> {
return observe(value).pipe(
Effect.map((entries): ReconcileResult => {
const updates: string[] = []
@ -253,7 +253,7 @@ export function reconcile(value: SystemContext, previous: Applied): Effect.Effec
}
/** Rebuilds the baseline, restating unobservable sources from the model's last-applied beliefs. */
export function rebaseline(value: SystemContext, previous: Applied): Effect.Effect<Baseline> {
export function rebaseline(value: Instructions, previous: Applied): Effect.Effect<Baseline> {
return observe(value).pipe(
Effect.map((entries): Baseline => {
const parts: string[] = []
@ -277,8 +277,8 @@ export function rebaseline(value: SystemContext, previous: Applied): Effect.Effe
)
}
function context(sources: ReadonlyArray<PackedSource>): SystemContext {
return { [ContextTypeId]: sources }
function instructions(sources: ReadonlyArray<PackedSource>): Instructions {
return { [InstructionsTypeId]: sources }
}
function render(parts: ReadonlyArray<string>) {
@ -294,7 +294,7 @@ function isUnavailable(value: unknown): value is Unavailable {
}
function requireText(key: Key, kind: string, text: string) {
if (text.length === 0) throw new Error(`System context source ${key} rendered an empty ${kind}`)
if (text.length === 0) throw new Error(`Instruction source ${key} rendered an empty ${kind}`)
return text
}

View file

@ -44,9 +44,9 @@ import { SessionTodo } from "./session/todo"
import { SkillV2 } from "./skill"
import { SkillGuidance } from "./skill/guidance"
import { Snapshot } from "./snapshot"
import { InstructionContext } from "./instruction-context"
import { SystemContextBuiltIns } from "./system-context/builtins"
import { SessionContextEntry } from "./session/context-entry"
import { InstructionDiscovery } from "./instruction-discovery"
import { InstructionBuiltIns } from "./instructions/builtins"
import { InstructionEntry } from "./session/instruction-entry"
import { SessionInstructions } from "./session/instructions"
import { McpTool } from "./tool/mcp"
import { ReadToolFileSystem } from "./tool/read-filesystem"
@ -112,8 +112,8 @@ const locationServiceNodes = [
Pty.node,
Shell.node,
SkillV2.node,
SystemContextBuiltIns.node,
InstructionContext.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationMutation.node,
FileMutation.node,
MCP.node,
@ -125,7 +125,7 @@ const locationServiceNodes = [
SkillGuidance.node,
ReferenceGuidance.node,
SessionTodo.node,
SessionContextEntry.node,
InstructionEntry.node,
Form.node,
QuestionV2.node,
Generate.node,

View file

@ -7,7 +7,7 @@ import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
import { McpTool } from "../tool/mcp"
import { MCP } from "./index"
import { SystemContext } from "../system-context/index"
import { Instructions } from "../instructions/index"
const Summary = Schema.Struct({
server: Schema.String,
@ -31,7 +31,7 @@ const render = (servers: ReadonlyArray<Summary>) =>
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
const diff = SystemContext.diffByKey(
const diff = Instructions.diffByKey(
previous,
current,
(server) => server.server,
@ -56,7 +56,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
}
export interface Interface {
readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpGuidance") {}
@ -69,9 +69,9 @@ export const layer = Layer.effect(
return Service.of({
load: Effect.fn("McpGuidance.load")(function* (selection) {
const agent = selection.info
if (!agent) return SystemContext.empty
if (!agent) return Instructions.empty
if (Flag.CODEMODE_ENABLED && PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny")
return SystemContext.empty
return Instructions.empty
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded",
})
@ -88,9 +88,9 @@ export const layer = Layer.effect(
)
})
.map((item) => ({ server: item.server, instructions: item.instructions }))
if (visible.length === 0) return SystemContext.empty
return SystemContext.make({
key: SystemContext.Key.make("core/mcp-guidance"),
if (visible.length === 0) return Instructions.empty
return Instructions.make({
key: Instructions.Key.make("core/mcp-guidance"),
codec: Schema.toCodecJson(Schema.Array(Summary)),
load: Effect.succeed(visible),
baseline: render,

View file

@ -3,7 +3,7 @@ export * as ReferenceGuidance from "./guidance"
import { makeLocationNode } from "../effect/app-node"
import { Context, Effect, Layer, Schema } from "effect"
import { Reference } from "../reference"
import { SystemContext } from "../system-context/index"
import { Instructions } from "../instructions/index"
const Summary = Schema.Struct({
name: Schema.String,
@ -29,7 +29,7 @@ const render = (references: ReadonlyArray<typeof Summary.Type>) =>
].join("\n")
const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyArray<typeof Summary.Type>) => {
const diff = SystemContext.diffByKey(
const diff = Instructions.diffByKey(
previous,
current,
(reference) => reference.name,
@ -54,7 +54,7 @@ const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyA
}
export interface Interface {
readonly load: () => Effect.Effect<SystemContext.SystemContext>
readonly load: () => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ReferenceGuidance") {}
@ -74,9 +74,9 @@ const layer = Layer.effect(
description: reference.description,
}))
.toSorted((a, b) => a.name.localeCompare(b.name))
if (available.length === 0) return SystemContext.empty
return SystemContext.make({
key: SystemContext.Key.make("core/reference-guidance"),
if (available.length === 0) return Instructions.empty
return Instructions.make({
key: Instructions.Key.make("core/reference-guidance"),
codec: Schema.toCodecJson(Schema.Array(Summary)),
load: Effect.succeed(available),
baseline: render,

View file

@ -4,7 +4,7 @@ import { Database } from "../database/database"
import { MessageDecodeError } from "./error"
import { SessionMessage } from "./message"
import { SessionSchema } from "./schema"
import { SessionContextCheckpointTable, SessionMessageTable } from "./sql"
import { InstructionCheckpointTable, SessionMessageTable } from "./sql"
type DatabaseService = Database.Interface["db"]
@ -70,9 +70,9 @@ export const load = Effect.fn("SessionHistory.load")(function* (db: DatabaseServ
const [epoch, compaction] = yield* Effect.all(
[
db
.select({ baselineSeq: SessionContextCheckpointTable.baseline_seq })
.from(SessionContextCheckpointTable)
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
.select({ baselineSeq: InstructionCheckpointTable.baseline_seq })
.from(InstructionCheckpointTable)
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.get()
.pipe(Effect.orDie),
latestCompaction(db, sessionID),

View file

@ -1,37 +1,37 @@
export * as SessionContextCheckpoint from "./context-checkpoint"
export * as InstructionCheckpoint from "./instruction-checkpoint"
import { eq } from "drizzle-orm"
import { Effect, Option, Schema } from "effect"
import type { Database } from "../database/database"
import { EventV2 } from "../event"
import { SystemContext } from "../system-context/index"
import { Instructions } from "../instructions/index"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionSchema } from "./schema"
import { SessionContextCheckpointTable } from "./sql"
import { InstructionCheckpointTable } from "./sql"
type DatabaseService = Database.Interface["db"]
const decodeApplied = Schema.decodeUnknownOption(SystemContext.Applied)
const decodeApplied = Schema.decodeUnknownOption(Instructions.Applied)
/**
* Loads or creates the session's durable context checkpoint, narrating any
* Loads or creates the session's durable instruction checkpoint, narrating any
* drift since the model was last told as a chronological update. Completed
* compaction rebaselines; nothing else rewrites the baseline. Runs before
* input promotion so a blocked first step leaves pending inputs untouched.
*/
export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* (
export const prepare = Effect.fn("InstructionCheckpoint.prepare")(function* (
db: DatabaseService,
events: EventV2.Interface,
context: Effect.Effect<SystemContext.SystemContext>,
instructions: Effect.Effect<Instructions.Instructions>,
sessionID: SessionSchema.ID,
) {
const [value, stored, compaction] = yield* Effect.all(
[context, find(db, sessionID), SessionHistory.latestCompaction(db, sessionID)],
[instructions, find(db, sessionID), SessionHistory.latestCompaction(db, sessionID)],
{ concurrency: "unbounded" },
)
if (!stored) {
const baseline = yield* SystemContext.initialize(value)
const baseline = yield* Instructions.initialize(value)
const baselineSeq = yield* insert(db, sessionID, baseline)
return { baseline: baseline.text, baselineSeq }
}
@ -40,28 +40,28 @@ export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* (
// treating every source as new, re-announcing baselines as updates.
const applied = Option.getOrElse(decodeApplied(stored.snapshot), () => ({}))
if (compaction !== undefined && compaction.seq > stored.baseline_seq) {
const baseline = yield* SystemContext.rebaseline(value, applied)
const baseline = yield* Instructions.rebaseline(value, applied)
yield* rewrite(db, sessionID, compaction.seq, baseline)
return { baseline: baseline.text, baselineSeq: compaction.seq }
}
const result = yield* SystemContext.reconcile(value, applied)
const result = yield* Instructions.reconcile(value, applied)
if (result._tag === "Unchanged") return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
yield* events.publish(
SessionEvent.ContextUpdated,
SessionEvent.InstructionsUpdated,
{ sessionID, text: result.text },
{ commit: () => advance(db, sessionID, result.applied).pipe(Effect.orDie) },
)
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
})
export const reset = Effect.fn("SessionContextCheckpoint.reset")(function* (
export const reset = Effect.fn("InstructionCheckpoint.reset")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
yield* db
.delete(SessionContextCheckpointTable)
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
.delete(InstructionCheckpointTable)
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.run()
.pipe(Effect.orDie)
})
@ -69,8 +69,8 @@ export const reset = Effect.fn("SessionContextCheckpoint.reset")(function* (
const find = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
return yield* db
.select()
.from(SessionContextCheckpointTable)
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
.from(InstructionCheckpointTable)
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.get()
.pipe(Effect.orDie)
})
@ -78,11 +78,11 @@ const find = Effect.fnUntraced(function* (db: DatabaseService, sessionID: Sessio
const insert = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
baseline: SystemContext.Baseline,
baseline: Instructions.Baseline,
) {
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
yield* db
.insert(SessionContextCheckpointTable)
.insert(InstructionCheckpointTable)
.values({
session_id: sessionID,
baseline: baseline.text,
@ -98,33 +98,33 @@ const rewrite = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
baselineSeq: number,
baseline: SystemContext.Baseline,
baseline: Instructions.Baseline,
) {
const updated = yield* db
.update(SessionContextCheckpointTable)
.update(InstructionCheckpointTable)
.set({
baseline: baseline.text,
snapshot: baseline.applied,
baseline_seq: baselineSeq,
})
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
.returning({ sessionID: SessionContextCheckpointTable.session_id })
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.returning({ sessionID: InstructionCheckpointTable.session_id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
if (!updated) return yield* Effect.die(new Error("Instruction checkpoint not found"))
})
const advance = Effect.fnUntraced(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
applied: SystemContext.Applied,
applied: Instructions.Applied,
) {
const updated = yield* db
.update(SessionContextCheckpointTable)
.update(InstructionCheckpointTable)
.set({ snapshot: applied })
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
.returning({ sessionID: SessionContextCheckpointTable.session_id })
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.returning({ sessionID: InstructionCheckpointTable.session_id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
if (!updated) return yield* Effect.die(new Error("Instruction checkpoint not found"))
})

View file

@ -1,17 +1,17 @@
export * as SessionContextEntry from "./context-entry"
export * as InstructionEntry from "./instruction-entry"
import { and, asc, eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { SessionContextEntry } from "@opencode-ai/schema/session-context-entry"
import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
import { Database } from "../database/database"
import { makeLocationNode } from "../effect/app-node"
import { SystemContext } from "../system-context/index"
import { Instructions } from "../instructions/index"
import { SessionSchema } from "./schema"
import { SessionContextEntryTable } from "./sql"
import { InstructionEntryTable } from "./sql"
export const Key = SessionContextEntry.Key
export const Key = InstructionEntry.Key
export type Key = typeof Key.Type
export const Info = SessionContextEntry.Info
export const Info = InstructionEntry.Info
export type Info = typeof Info.Type
export interface Interface {
@ -22,11 +22,11 @@ export interface Interface {
readonly value: Schema.Json
}) => Effect.Effect<void>
readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect<void>
/** Produces one SystemContext source per stored entry, keyed `api/<key>`. */
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<SystemContext.SystemContext>
/** Produces one Instructions source per stored entry, keyed `api/<key>`. */
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContextEntry") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionEntry") {}
const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2))
@ -36,8 +36,8 @@ const renderBlock = (key: Key, value: Schema.Json) =>
// Rendering stays mechanism-neutral: the model sees session context, not how
// it was attached. Only chronological updates and removals carry narration.
const source = (entry: Info) =>
SystemContext.make({
key: SystemContext.Key.make(`api/${entry.key}`),
Instructions.make({
key: Instructions.Key.make(`api/${entry.key}`),
codec: Schema.toCodecJson(Schema.Json),
load: Effect.succeed(entry.value),
baseline: (value) => renderBlock(entry.key, value),
@ -54,49 +54,47 @@ const layer = Layer.effect(
Effect.gen(function* () {
const { db } = yield* Database.Service
const list = Effect.fn("SessionContextEntry.list")(function* (sessionID: SessionSchema.ID) {
const list = Effect.fn("InstructionEntry.list")(function* (sessionID: SessionSchema.ID) {
const rows = yield* db
.select()
.from(SessionContextEntryTable)
.where(eq(SessionContextEntryTable.session_id, sessionID))
.orderBy(asc(SessionContextEntryTable.key))
.from(InstructionEntryTable)
.where(eq(InstructionEntryTable.session_id, sessionID))
.orderBy(asc(InstructionEntryTable.key))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({ key: row.key, value: row.value }))
})
const put = Effect.fn("SessionContextEntry.put")(function* (input: {
const put = Effect.fn("InstructionEntry.put")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly key: Key
readonly value: Schema.Json
}) {
yield* db
.insert(SessionContextEntryTable)
.insert(InstructionEntryTable)
.values({ session_id: input.sessionID, key: input.key, value: input.value })
.onConflictDoUpdate({
target: [SessionContextEntryTable.session_id, SessionContextEntryTable.key],
target: [InstructionEntryTable.session_id, InstructionEntryTable.key],
set: { value: input.value, time_updated: Date.now() },
})
.run()
.pipe(Effect.orDie)
})
const remove = Effect.fn("SessionContextEntry.remove")(function* (input: {
const remove = Effect.fn("InstructionEntry.remove")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly key: Key
}) {
yield* db
.delete(SessionContextEntryTable)
.where(
and(eq(SessionContextEntryTable.session_id, input.sessionID), eq(SessionContextEntryTable.key, input.key)),
)
.delete(InstructionEntryTable)
.where(and(eq(InstructionEntryTable.session_id, input.sessionID), eq(InstructionEntryTable.key, input.key)))
.run()
.pipe(Effect.orDie)
})
const load = Effect.fn("SessionContextEntry.load")(function* (sessionID: SessionSchema.ID) {
const load = Effect.fn("InstructionEntry.load")(function* (sessionID: SessionSchema.ID) {
const entries = yield* list(sessionID)
return SystemContext.combine(entries.map(source))
return Instructions.combine(entries.map(source))
})
return Service.of({ list, put, remove, load })

View file

@ -145,7 +145,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
"session.prompt.promoted": () => Effect.void,
"session.prompt.admitted": () => Effect.void,
"session.execution.settled": () => Effect.void,
"session.context.updated": (event) =>
"session.instructions.updated": (event) =>
adapter.appendMessage(
SessionMessage.System.make({
id: SessionMessage.ID.fromEvent(event.id),
@ -154,6 +154,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
time: { created: event.created },
}),
),
"session.instructions.discovered": () => Effect.void,
"session.synthetic": (event) => {
return adapter.appendMessage(
SessionMessage.Synthetic.make({

View file

@ -13,11 +13,11 @@ import { SessionMessage } from "./message"
import { SessionMessageUpdater } from "./message-updater"
import { SessionInput } from "./input"
import { WorkspaceV2 } from "../workspace"
import { SessionContextCheckpoint } from "./context-checkpoint"
import { InstructionCheckpoint } from "./instruction-checkpoint"
import {
MessageTable,
PartTable,
SessionContextCheckpointTable,
InstructionCheckpointTable,
SessionInputTable,
SessionMessageTable,
SessionTable,
@ -220,13 +220,13 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
// folding at the same baseline horizon.
const checkpoint = yield* db
.select()
.from(SessionContextCheckpointTable)
.where(eq(SessionContextCheckpointTable.session_id, event.data.parentID))
.from(InstructionCheckpointTable)
.where(eq(InstructionCheckpointTable.session_id, event.data.parentID))
.get()
.pipe(Effect.orDie)
if (checkpoint) {
yield* db
.insert(SessionContextCheckpointTable)
.insert(InstructionCheckpointTable)
.values({ ...checkpoint, session_id: event.data.sessionID })
.run()
.pipe(Effect.orDie)
@ -497,7 +497,7 @@ const layer = Layer.effectDiscard(
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
yield* SessionContextCheckpoint.reset(db, event.data.sessionID)
yield* InstructionCheckpoint.reset(db, event.data.sessionID)
}),
)
yield* events.project(SessionV1.Event.Deleted, (event) =>
@ -634,7 +634,7 @@ const layer = Layer.effectDiscard(
})
}),
)
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
yield* events.project(SessionEvent.InstructionsUpdated, (event) => run(db, event))
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
yield* events.project(SessionEvent.Skill.Activated, (event) =>
insertMessage(db, event, {
@ -718,7 +718,7 @@ const layer = Layer.effectDiscard(
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
yield* SessionContextCheckpoint.reset(db, event.data.sessionID)
yield* InstructionCheckpoint.reset(db, event.data.sessionID)
}),
)
}),

View file

@ -5,11 +5,11 @@ import { Context, Effect } from "effect"
import { SessionSchema } from "../schema"
import type { MessageDecodeError } from "../error"
import { SessionRunnerModel } from "./model"
import type { SystemContext } from "../../system-context/index"
import type { Instructions } from "../../instructions/index"
import type { ToolOutputStore } from "../../tool-output-store"
export type RunError =
LLMError | SessionRunnerModel.Error | MessageDecodeError | SystemContext.InitializationBlocked | ToolOutputStore.Error
LLMError | SessionRunnerModel.Error | MessageDecodeError | Instructions.InitializationBlocked | ToolOutputStore.Error
/** Runs one local continuation from already-recorded Session history. */
export interface Interface {

View file

@ -10,23 +10,23 @@ import {
isContextOverflowFailure,
type ProviderErrorEvent,
} from "@opencode-ai/llm"
import { Cause, DateTime, Effect, Exit, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { Cause, Effect, Exit, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Config } from "../../config"
import { Database } from "../../database/database"
import { EventV2 } from "../../event"
import { Location } from "../../location"
import { SystemContext } from "../../system-context/index"
import { SystemContextBuiltIns } from "../../system-context/builtins"
import { InstructionContext } from "../../instruction-context"
import { Instructions } from "../../instructions/index"
import { InstructionBuiltIns } from "../../instructions/builtins"
import { InstructionDiscovery } from "../../instruction-discovery"
import { SkillGuidance } from "../../skill/guidance"
import { ReferenceGuidance } from "../../reference/guidance"
import { McpGuidance } from "../../mcp/guidance"
import { SessionContextEntry } from "../context-entry"
import { InstructionEntry } from "../instruction-entry"
import { QuestionTool } from "../../tool/question"
import { ToolRegistry } from "../../tool/registry"
import { ToolOutputStore } from "../../tool-output-store"
import { SessionContextCheckpoint } from "../context-checkpoint"
import { InstructionCheckpoint } from "../instruction-checkpoint"
import { SessionCompaction } from "../compaction"
import { SessionEvent } from "../event"
import { SessionHistory } from "../history"
@ -104,12 +104,12 @@ const layer = Layer.effect(
const models = yield* SessionRunnerModel.Service
const store = yield* SessionStore.Service
const location = yield* Location.Service
const builtins = yield* SystemContextBuiltIns.Service
const instructions = yield* InstructionContext.Service
const builtins = yield* InstructionBuiltIns.Service
const discovery = yield* InstructionDiscovery.Service
const skillGuidance = yield* SkillGuidance.Service
const referenceGuidance = yield* ReferenceGuidance.Service
const mcpGuidance = yield* McpGuidance.Service
const contextEntries = yield* SessionContextEntry.Service
const entries = yield* InstructionEntry.Service
const snapshots = yield* Snapshot.Service
const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service
@ -152,18 +152,18 @@ const layer = Layer.effect(
const isQuestionCancelled = (cause: Cause.Cause<unknown>) =>
cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionTool.CancelledError)
const loadSystemContext = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
const loadInstructions = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
Effect.all(
[
builtins.load(),
instructions.load(),
discovery.load(),
skillGuidance.load(agent),
referenceGuidance.load(),
mcpGuidance.load(agent),
contextEntries.load(sessionID),
entries.load(sessionID),
],
{ concurrency: "unbounded" },
).pipe(Effect.map(SystemContext.combine))
).pipe(Effect.map(Instructions.combine))
const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* (
sessionID: SessionSchema.ID,
@ -177,10 +177,10 @@ const layer = Layer.effect(
const agent = yield* agents.select(session.agent)
// Establish what the model knows before admitting what the user said, so
// a blocked first step leaves pending inputs untouched.
const checkpoint = yield* SessionContextCheckpoint.prepare(
const checkpoint = yield* InstructionCheckpoint.prepare(
db,
events,
loadSystemContext(agent, session.id),
loadInstructions(agent, session.id),
session.id,
)
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
@ -458,12 +458,12 @@ export const node = makeLocationNode({
SessionRunnerModel.node,
SessionStore.node,
Location.node,
SystemContextBuiltIns.node,
InstructionContext.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
SkillGuidance.node,
ReferenceGuidance.node,
McpGuidance.node,
SessionContextEntry.node,
InstructionEntry.node,
SessionCompaction.node,
SessionTitle.node,
Config.node,

View file

@ -11,8 +11,7 @@ import type { SessionSchema } from "./schema"
import type { MessageID, PartID, SessionV1 } from "../v1/session"
import { WorkspaceV2 } from "../workspace"
import { Timestamps } from "../database/schema.sql"
import type { SystemContext } from "../system-context/index"
import { AgentV2 } from "../agent"
import type { Instructions } from "../instructions/index"
import type { Revert } from "@opencode-ai/schema/revert"
import type { Schema } from "effect"
@ -166,8 +165,8 @@ export const SessionInputTable = sqliteTable(
],
)
export const SessionContextEntryTable = sqliteTable(
"session_context_entry",
export const InstructionEntryTable = sqliteTable(
"instruction_entry",
{
session_id: text()
.$type<SessionSchema.ID>()
@ -180,12 +179,12 @@ export const SessionContextEntryTable = sqliteTable(
(table) => [primaryKey({ columns: [table.session_id, table.key] })],
)
export const SessionContextCheckpointTable = sqliteTable("session_context_epoch", {
export const InstructionCheckpointTable = sqliteTable("instruction_checkpoint", {
session_id: text()
.$type<SessionSchema.ID>()
.primaryKey()
.references(() => SessionTable.id, { onDelete: "cascade" }),
baseline: text().notNull(),
snapshot: text({ mode: "json" }).notNull().$type<SystemContext.Applied>(),
snapshot: text({ mode: "json" }).notNull().$type<Instructions.Applied>(),
baseline_seq: integer().notNull(),
})

View file

@ -5,7 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
import { SkillV2 } from "../skill"
import { SystemContext } from "../system-context/index"
import { Instructions } from "../instructions/index"
const Summary = Schema.Struct({
name: Schema.String,
@ -31,7 +31,7 @@ const render = (skills: ReadonlyArray<Summary>) =>
].join("\n")
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
const diff = SystemContext.diffByKey(
const diff = Instructions.diffByKey(
previous,
current,
(skill) => skill.name,
@ -56,7 +56,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
}
export interface Interface {
readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SkillGuidance") {}
@ -69,10 +69,10 @@ const layer = Layer.effect(
return Service.of({
load: Effect.fn("SkillGuidance.load")(function* (selection) {
const agent = selection.info
if (!agent) return SystemContext.empty
if (!agent) return Instructions.empty
const permitted = SkillV2.available(yield* skills.list(), agent)
if (permitted.length === 0 && PermissionV2.evaluate("skill", "*", agent.permissions).effect === "deny")
return SystemContext.empty
return Instructions.empty
const available = permitted
.flatMap((skill) =>
skill.description === undefined || skill.autoinvoke === false
@ -80,8 +80,8 @@ const layer = Layer.effect(
: [{ name: skill.name, description: skill.description }],
)
.toSorted((a, b) => a.name.localeCompare(b.name))
return SystemContext.make({
key: SystemContext.Key.make("core/skill-guidance"),
return Instructions.make({
key: Instructions.Key.make("core/skill-guidance"),
codec: Schema.toCodecJson(Schema.Array(Summary)),
load: Effect.succeed(available),
baseline: render,

View file

@ -23,11 +23,11 @@ export const Output = Schema.Struct({
})
export const description = [
"Load a specialized skill when the task at hand matches one of the available skills in the system context.",
"Load a specialized skill when the task at hand matches one of the available skills in the instructions.",
"",
"Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.",
"",
"The skill name must match one of the available skills in the system context.",
"The skill name must match one of the available skills in the instructions.",
].join("\n")
export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray<string>) => {

View file

@ -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",
})

View file

@ -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" },

View file

@ -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({

View 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" })
}),
)
})

View file

@ -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()

View file

@ -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.",

View file

@ -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()
}),
)

View file

@ -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],

View file

@ -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 })

View file

@ -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])))
})
})

View file

@ -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"),
)
}),
)
})