refactor(core): rename system context to instructions (#35583)
This commit is contained in:
parent
1a52e1118e
commit
91f1815732
62 changed files with 1482 additions and 1005 deletions
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
),
|
||||
})
|
||||
}),
|
||||
|
|
@ -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) })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
})
|
||||
|
|
@ -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 })
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue