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

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