mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 00:36:20 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 417c88fc40 | |||
| 860604ec4c | |||
| 29c29739b5 |
@@ -34,6 +34,7 @@ const layer = Layer.effect(
|
|||||||
const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
|
const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key,
|
key,
|
||||||
|
description: "Ambient instructions",
|
||||||
codec: Schema.toCodecJson(Files),
|
codec: Schema.toCodecJson(Files),
|
||||||
load: Effect.succeed(value),
|
load: Effect.succeed(value),
|
||||||
baseline: render,
|
baseline: render,
|
||||||
|
|||||||
@@ -31,22 +31,35 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
|||||||
(server) => server.server,
|
(server) => server.server,
|
||||||
(before, after) => before.instructions !== after.instructions,
|
(before, after) => before.instructions !== after.instructions,
|
||||||
)
|
)
|
||||||
|
const items = SystemContext.diffItems(diff, (server) => ({
|
||||||
|
key: server.server,
|
||||||
|
description: "MCP server instructions",
|
||||||
|
}))
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// Additions and removals render as small deltas; anything else restates the full list.
|
||||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||||
return [
|
return {
|
||||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
text: [
|
||||||
render(current),
|
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||||
].join("\n")
|
render(current),
|
||||||
return [
|
].join("\n"),
|
||||||
...(diff.added.length === 0
|
items,
|
||||||
? []
|
}
|
||||||
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(diff.added)]),
|
return {
|
||||||
...(diff.removed.length === 0
|
text: [
|
||||||
? []
|
...(diff.added.length === 0
|
||||||
: [
|
? []
|
||||||
`Instructions for the following MCP servers are no longer available: ${diff.removed.map((server) => server.server).join(", ")}.`,
|
: [
|
||||||
]),
|
"New MCP server instructions are available in addition to those previously listed:",
|
||||||
].join("\n")
|
...entries(diff.added),
|
||||||
|
]),
|
||||||
|
...(diff.removed.length === 0
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
`Instructions for the following MCP servers are no longer available: ${diff.removed.map((server) => server.server).join(", ")}.`,
|
||||||
|
]),
|
||||||
|
].join("\n"),
|
||||||
|
items,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -83,6 +96,7 @@ export const layer = Layer.effect(
|
|||||||
if (visible.length === 0) return SystemContext.empty
|
if (visible.length === 0) return SystemContext.empty
|
||||||
return SystemContext.make({
|
return SystemContext.make({
|
||||||
key: SystemContext.Key.make("core/mcp-guidance"),
|
key: SystemContext.Key.make("core/mcp-guidance"),
|
||||||
|
description: "MCP server instructions",
|
||||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||||
load: Effect.succeed(visible),
|
load: Effect.succeed(visible),
|
||||||
baseline: render,
|
baseline: render,
|
||||||
|
|||||||
@@ -35,22 +35,32 @@ const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyA
|
|||||||
(reference) => reference.name,
|
(reference) => reference.name,
|
||||||
(before, after) => before.path !== after.path || before.description !== after.description,
|
(before, after) => before.path !== after.path || before.description !== after.description,
|
||||||
)
|
)
|
||||||
|
const items = SystemContext.diffItems(diff, (reference) => ({
|
||||||
|
key: reference.name,
|
||||||
|
description: reference.description ?? reference.path,
|
||||||
|
}))
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// Additions and removals render as small deltas; anything else restates the full list.
|
||||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||||
return [
|
return {
|
||||||
"The available project references have changed. This list supersedes the previous reference list.",
|
text: [
|
||||||
render(current),
|
"The available project references have changed. This list supersedes the previous reference list.",
|
||||||
].join("\n")
|
render(current),
|
||||||
return [
|
].join("\n"),
|
||||||
...(diff.added.length === 0
|
items,
|
||||||
? []
|
}
|
||||||
: ["New project references are available in addition to those previously listed:", ...entries(diff.added)]),
|
return {
|
||||||
...(diff.removed.length === 0
|
text: [
|
||||||
? []
|
...(diff.added.length === 0
|
||||||
: [
|
? []
|
||||||
`The following project references are no longer available and must not be used: ${diff.removed.map((reference) => reference.name).join(", ")}.`,
|
: ["New project references are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||||
]),
|
...(diff.removed.length === 0
|
||||||
].join("\n")
|
? []
|
||||||
|
: [
|
||||||
|
`The following project references are no longer available and must not be used: ${diff.removed.map((reference) => reference.name).join(", ")}.`,
|
||||||
|
]),
|
||||||
|
].join("\n"),
|
||||||
|
items,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -77,6 +87,7 @@ const layer = Layer.effect(
|
|||||||
if (available.length === 0) return SystemContext.empty
|
if (available.length === 0) return SystemContext.empty
|
||||||
return SystemContext.make({
|
return SystemContext.make({
|
||||||
key: SystemContext.Key.make("core/reference-guidance"),
|
key: SystemContext.Key.make("core/reference-guidance"),
|
||||||
|
description: "Project references",
|
||||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||||
load: Effect.succeed(available),
|
load: Effect.succeed(available),
|
||||||
baseline: render,
|
baseline: render,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const renderBlock = (key: Key, value: Schema.Json) =>
|
|||||||
const source = (entry: Info) =>
|
const source = (entry: Info) =>
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: SystemContext.Key.make(`api/${entry.key}`),
|
key: SystemContext.Key.make(`api/${entry.key}`),
|
||||||
|
description: `Session context: ${entry.key}`,
|
||||||
codec: Schema.toCodecJson(Schema.Json),
|
codec: Schema.toCodecJson(Schema.Json),
|
||||||
load: Effect.succeed(entry.value),
|
load: Effect.succeed(entry.value),
|
||||||
baseline: (value) => renderBlock(entry.key, value),
|
baseline: (value) => renderBlock(entry.key, value),
|
||||||
|
|||||||
@@ -37,22 +37,29 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
|||||||
(skill) => skill.name,
|
(skill) => skill.name,
|
||||||
(before, after) => before.description !== after.description,
|
(before, after) => before.description !== after.description,
|
||||||
)
|
)
|
||||||
|
const items = SystemContext.diffItems(diff, (skill) => ({ key: skill.name, description: skill.description }))
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// Additions and removals render as small deltas; anything else restates the full list.
|
||||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||||
return [
|
return {
|
||||||
"The available skills have changed. This list supersedes the previous available skills list.",
|
text: [
|
||||||
render(current),
|
"The available skills have changed. This list supersedes the previous available skills list.",
|
||||||
].join("\n")
|
render(current),
|
||||||
return [
|
].join("\n"),
|
||||||
...(diff.added.length === 0
|
items,
|
||||||
? []
|
}
|
||||||
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
return {
|
||||||
...(diff.removed.length === 0
|
text: [
|
||||||
? []
|
...(diff.added.length === 0
|
||||||
: [
|
? []
|
||||||
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
|
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||||
]),
|
...(diff.removed.length === 0
|
||||||
].join("\n")
|
? []
|
||||||
|
: [
|
||||||
|
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
|
||||||
|
]),
|
||||||
|
].join("\n"),
|
||||||
|
items,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -82,6 +89,7 @@ const layer = Layer.effect(
|
|||||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
return SystemContext.make({
|
return SystemContext.make({
|
||||||
key: SystemContext.Key.make("core/skill-guidance"),
|
key: SystemContext.Key.make("core/skill-guidance"),
|
||||||
|
description: "Available skills",
|
||||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||||
load: Effect.succeed(available),
|
load: Effect.succeed(available),
|
||||||
baseline: render,
|
baseline: render,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const layer = Layer.effect(
|
|||||||
const context = SystemContext.combine([
|
const context = SystemContext.combine([
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: SystemContext.Key.make("core/environment"),
|
key: SystemContext.Key.make("core/environment"),
|
||||||
|
description: "Environment",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: Effect.succeed(environment),
|
load: Effect.succeed(environment),
|
||||||
baseline: (environment) =>
|
baseline: (environment) =>
|
||||||
@@ -34,6 +35,7 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: SystemContext.Key.make("core/date"),
|
key: SystemContext.Key.make("core/date"),
|
||||||
|
description: "Current date",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: DateTime.nowAsDate.pipe(Effect.map((date) => date.toDateString())),
|
load: DateTime.nowAsDate.pipe(Effect.map((date) => date.toDateString())),
|
||||||
baseline: (date) => `Today's date: ${date}`,
|
baseline: (date) => `Today's date: ${date}`,
|
||||||
|
|||||||
@@ -36,13 +36,34 @@ export type Unavailable = typeof unavailable
|
|||||||
/** Defines one typed source before its value type is hidden by `make`. */
|
/** Defines one typed source before its value type is hidden by `make`. */
|
||||||
export interface Source<A> {
|
export interface Source<A> {
|
||||||
readonly key: Key
|
readonly key: Key
|
||||||
readonly codec: Schema.Codec<A, Schema.Json, never, never>
|
readonly description: string
|
||||||
|
readonly codec: Schema.Codec<A, Schema.Json>
|
||||||
readonly load: Effect.Effect<A | Unavailable>
|
readonly load: Effect.Effect<A | Unavailable>
|
||||||
readonly baseline: (current: A) => string
|
readonly baseline: (current: A) => string
|
||||||
readonly update: (previous: A, current: A) => string
|
readonly update: (previous: A, current: A) => string | StructuredUpdate
|
||||||
readonly removed?: (previous: A) => string
|
readonly removed?: (previous: A) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReconcileAction = "added" | "updated" | "removed"
|
||||||
|
|
||||||
|
export interface ReconcileItemUpdate {
|
||||||
|
readonly key: string
|
||||||
|
readonly description: string
|
||||||
|
readonly action: ReconcileAction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReconcileUpdate {
|
||||||
|
readonly key: Key
|
||||||
|
readonly description: string
|
||||||
|
readonly action: ReconcileAction
|
||||||
|
readonly items?: ReadonlyArray<ReconcileItemUpdate>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StructuredUpdate {
|
||||||
|
readonly text: string
|
||||||
|
readonly items?: ReadonlyArray<ReconcileItemUpdate>
|
||||||
|
}
|
||||||
|
|
||||||
const ContextTypeId: unique symbol = Symbol.for("@opencode/SystemContext")
|
const ContextTypeId: unique symbol = Symbol.for("@opencode/SystemContext")
|
||||||
|
|
||||||
/** Opaque carrier for composable system context sources. */
|
/** Opaque carrier for composable system context sources. */
|
||||||
@@ -53,6 +74,7 @@ export interface SystemContext {
|
|||||||
/** The value last applied to the model for one admitted source. */
|
/** The value last applied to the model for one admitted source. */
|
||||||
export const AppliedSource = Schema.Struct({
|
export const AppliedSource = Schema.Struct({
|
||||||
value: Schema.Json,
|
value: Schema.Json,
|
||||||
|
description: Schema.optional(Schema.NonEmptyString),
|
||||||
removed: Schema.optional(Schema.NonEmptyString),
|
removed: Schema.optional(Schema.NonEmptyString),
|
||||||
})
|
})
|
||||||
export type AppliedSource = typeof AppliedSource.Type
|
export type AppliedSource = typeof AppliedSource.Type
|
||||||
@@ -70,6 +92,7 @@ export interface Baseline {
|
|||||||
export interface Updated {
|
export interface Updated {
|
||||||
readonly _tag: "Updated"
|
readonly _tag: "Updated"
|
||||||
readonly text: string
|
readonly text: string
|
||||||
|
readonly updates: ReadonlyArray<ReconcileUpdate>
|
||||||
readonly applied: Applied
|
readonly applied: Applied
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +123,11 @@ interface PackedSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Observed {
|
interface Observed {
|
||||||
|
readonly description: string
|
||||||
readonly applied: AppliedSource
|
readonly applied: AppliedSource
|
||||||
readonly baseline: () => string
|
readonly baseline: () => string
|
||||||
/** `undefined` means unchanged. An undecodable previous value re-renders the baseline (treat-as-new). */
|
/** `undefined` means unchanged. An undecodable previous value re-renders the baseline (treat-as-new). */
|
||||||
readonly update: (previous: AppliedSource) => string | undefined
|
readonly update: (previous: AppliedSource) => StructuredUpdate | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Entry {
|
interface Entry {
|
||||||
@@ -120,6 +144,7 @@ export function make<A>(source: Source<A>): SystemContext {
|
|||||||
const decode = Schema.decodeUnknownOption(source.codec)
|
const decode = Schema.decodeUnknownOption(source.codec)
|
||||||
const encode = Schema.encodeSync(source.codec)
|
const encode = Schema.encodeSync(source.codec)
|
||||||
const equivalent = Schema.toEquivalence(source.codec)
|
const equivalent = Schema.toEquivalence(source.codec)
|
||||||
|
const description = requireText(source.key, "description", source.description)
|
||||||
const baseline = (value: A) => requireText(source.key, "baseline", source.baseline(value))
|
const baseline = (value: A) => requireText(source.key, "baseline", source.baseline(value))
|
||||||
return context([
|
return context([
|
||||||
{
|
{
|
||||||
@@ -133,18 +158,19 @@ export function make<A>(source: Source<A>): SystemContext {
|
|||||||
Effect.map((value) => {
|
Effect.map((value) => {
|
||||||
if (isUnavailable(value)) return value
|
if (isUnavailable(value)) return value
|
||||||
return {
|
return {
|
||||||
|
description,
|
||||||
applied: {
|
applied: {
|
||||||
value: encode(value),
|
value: encode(value),
|
||||||
...(source.removed ? { removed: requireText(source.key, "removal", source.removed(value)) } : {}),
|
...(source.removed
|
||||||
|
? { description, removed: requireText(source.key, "removal", source.removed(value)) }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
baseline: () => baseline(value),
|
baseline: () => baseline(value),
|
||||||
update: (previous) =>
|
update: (previous) =>
|
||||||
Option.match(decode(previous.value), {
|
Option.match(decode(previous.value), {
|
||||||
onNone: () => baseline(value),
|
onNone: () => ({ text: baseline(value) }),
|
||||||
onSome: (decoded) =>
|
onSome: (decoded) =>
|
||||||
equivalent(decoded, value)
|
equivalent(decoded, value) ? undefined : normalizeUpdate(source.key, source.update(decoded, value)),
|
||||||
? undefined
|
|
||||||
: requireText(source.key, "update", source.update(decoded, value)),
|
|
||||||
}),
|
}),
|
||||||
} satisfies Observed
|
} satisfies Observed
|
||||||
}),
|
}),
|
||||||
@@ -179,6 +205,17 @@ export function diffByKey<A>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function diffItems<A>(
|
||||||
|
diff: ReturnType<typeof diffByKey<A>>,
|
||||||
|
item: (value: A) => { readonly key: string; readonly description: string },
|
||||||
|
): ReadonlyArray<ReconcileItemUpdate> {
|
||||||
|
return [
|
||||||
|
...diff.added.map((value) => ({ ...item(value), action: "added" as const })),
|
||||||
|
...diff.removed.map((value) => ({ ...item(value), action: "removed" as const })),
|
||||||
|
...diff.changed.map((value) => ({ ...item(value.current), action: "updated" as const })),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/** Combines contexts in order and rejects duplicate source keys immediately. */
|
/** Combines contexts in order and rejects duplicate source keys immediately. */
|
||||||
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
||||||
const sources = values.flatMap((value) => value[ContextTypeId])
|
const sources = values.flatMap((value) => value[ContextTypeId])
|
||||||
@@ -216,7 +253,8 @@ export function initialize(value: SystemContext): Effect.Effect<Baseline, Initia
|
|||||||
export function reconcile(value: SystemContext, previous: Applied): Effect.Effect<ReconcileResult> {
|
export function reconcile(value: SystemContext, previous: Applied): Effect.Effect<ReconcileResult> {
|
||||||
return observe(value).pipe(
|
return observe(value).pipe(
|
||||||
Effect.map((entries): ReconcileResult => {
|
Effect.map((entries): ReconcileResult => {
|
||||||
const updates: string[] = []
|
const parts: string[] = []
|
||||||
|
const updates: ReconcileUpdate[] = []
|
||||||
const applied: Record<string, AppliedSource> = {}
|
const applied: Record<string, AppliedSource> = {}
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const stored = get(previous, entry.key)
|
const stored = get(previous, entry.key)
|
||||||
@@ -226,16 +264,23 @@ export function reconcile(value: SystemContext, previous: Applied): Effect.Effec
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
updates.push(entry.observed.baseline())
|
parts.push(entry.observed.baseline())
|
||||||
|
updates.push({ key: entry.key, description: entry.observed.description, action: "added" })
|
||||||
applied[entry.key] = entry.observed.applied
|
applied[entry.key] = entry.observed.applied
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const text = entry.observed.update(stored)
|
const update = entry.observed.update(stored)
|
||||||
if (text === undefined) {
|
if (update === undefined) {
|
||||||
applied[entry.key] = stored
|
applied[entry.key] = stored
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
updates.push(text)
|
parts.push(update.text)
|
||||||
|
updates.push({
|
||||||
|
key: entry.key,
|
||||||
|
description: entry.observed.description,
|
||||||
|
action: "updated",
|
||||||
|
...(update.items === undefined ? {} : { items: update.items }),
|
||||||
|
})
|
||||||
applied[entry.key] = entry.observed.applied
|
applied[entry.key] = entry.observed.applied
|
||||||
}
|
}
|
||||||
const keys = new Set<string>(entries.map((entry) => entry.key))
|
const keys = new Set<string>(entries.map((entry) => entry.key))
|
||||||
@@ -244,10 +289,17 @@ export function reconcile(value: SystemContext, previous: Applied): Effect.Effec
|
|||||||
const removed = previous[key].removed
|
const removed = previous[key].removed
|
||||||
// An unannounced removal retains the belief; it clears at the next rebaseline.
|
// An unannounced removal retains the belief; it clears at the next rebaseline.
|
||||||
if (removed === undefined) applied[key] = previous[key]
|
if (removed === undefined) applied[key] = previous[key]
|
||||||
else updates.push(removed)
|
else {
|
||||||
|
parts.push(removed)
|
||||||
|
updates.push({
|
||||||
|
key: Key.make(key),
|
||||||
|
description: previous[key].description ?? key,
|
||||||
|
action: "removed",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (updates.length === 0) return { _tag: "Unchanged" }
|
if (updates.length === 0) return { _tag: "Unchanged" }
|
||||||
return { _tag: "Updated", text: render(updates), applied }
|
return { _tag: "Updated", text: render(parts), updates, applied }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -298,6 +350,11 @@ function requireText(key: Key, kind: string, text: string) {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeUpdate(key: Key, update: string | StructuredUpdate) {
|
||||||
|
if (typeof update === "string") return { text: requireText(key, "update", update) }
|
||||||
|
return { ...update, text: requireText(key, "update", update.text) }
|
||||||
|
}
|
||||||
|
|
||||||
function assertUniqueKeys(sources: ReadonlyArray<PackedSource>) {
|
function assertUniqueKeys(sources: ReadonlyArray<PackedSource>) {
|
||||||
const keys = new Set<Key>()
|
const keys = new Set<Key>()
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ describe("InstructionContext", () => {
|
|||||||
`Instructions from: ${globalFile}\nglobal`,
|
`Instructions from: ${globalFile}\nglobal`,
|
||||||
`Instructions from: ${projectFile}\nproject`,
|
`Instructions from: ${projectFile}\nproject`,
|
||||||
].join("\n\n"),
|
].join("\n\n"),
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
key: SystemContext.Key.make("core/instructions"),
|
||||||
|
description: "Ambient instructions",
|
||||||
|
action: "updated",
|
||||||
|
},
|
||||||
|
],
|
||||||
applied: expect.any(Object),
|
applied: expect.any(Object),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -101,6 +108,13 @@ describe("InstructionContext", () => {
|
|||||||
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toEqual({
|
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "Previously loaded instructions no longer apply.",
|
text: "Previously loaded instructions no longer apply.",
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
key: SystemContext.Key.make("core/instructions"),
|
||||||
|
description: "Ambient instructions",
|
||||||
|
action: "removed",
|
||||||
|
},
|
||||||
|
],
|
||||||
applied: {},
|
applied: {},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ const systemContext = Layer.mock(SystemContextBuiltIns.Service, {
|
|||||||
: [
|
: [
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: systemContextKey,
|
key: systemContextKey,
|
||||||
|
description: "Test context",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: systemLoadHook.pipe(
|
load: systemLoadHook.pipe(
|
||||||
Effect.andThen(Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline))),
|
Effect.andThen(Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline))),
|
||||||
@@ -205,6 +206,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
|||||||
skillBaselines.has(agent.id)
|
skillBaselines.has(agent.id)
|
||||||
? SystemContext.make({
|
? SystemContext.make({
|
||||||
key: SystemContext.Key.make("test/skill-guidance"),
|
key: SystemContext.Key.make("test/skill-guidance"),
|
||||||
|
description: "Test skill guidance",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: Effect.succeed(skillBaselines.get(agent.id)!),
|
load: Effect.succeed(skillBaselines.get(agent.id)!),
|
||||||
baseline: String,
|
baseline: String,
|
||||||
@@ -809,7 +811,9 @@ describe("SessionRunnerLLM", () => {
|
|||||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
expect(healed?.snapshot).toEqual({ "test/context": { value: "Initial context", removed: expect.any(String) } })
|
expect(healed?.snapshot).toEqual({
|
||||||
|
"test/context": { value: "Initial context", description: "Test context", removed: expect.any(String) },
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import { Cause, Effect, Exit, Schema } from "effect"
|
|||||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
|
|
||||||
const key = SystemContext.Key.make
|
const key = (value: string) => SystemContext.Key.make(value)
|
||||||
const stringContext = (input: {
|
const stringContext = (input: {
|
||||||
key: string
|
key: string
|
||||||
value: string | SystemContext.Unavailable
|
value: string | SystemContext.Unavailable
|
||||||
|
description?: string
|
||||||
baseline?: (value: string) => string
|
baseline?: (value: string) => string
|
||||||
update?: (previous: string, current: string) => string
|
update?: (previous: string, current: string) => string | SystemContext.StructuredUpdate
|
||||||
removed?: (value: string) => string
|
removed?: (value: string) => string
|
||||||
}) =>
|
}) =>
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: key(input.key),
|
key: key(input.key),
|
||||||
|
description: input.description ?? `Description for ${input.key}`,
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: Effect.succeed(input.value),
|
load: Effect.succeed(input.value),
|
||||||
baseline: input.baseline ?? String,
|
baseline: input.baseline ?? String,
|
||||||
@@ -25,6 +27,7 @@ describe("SystemContext", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const context = SystemContext.make({
|
const context = SystemContext.make({
|
||||||
key: key("core/date"),
|
key: key("core/date"),
|
||||||
|
description: "Current date",
|
||||||
codec: Schema.toCodecJson(Schema.DateFromString),
|
codec: Schema.toCodecJson(Schema.DateFromString),
|
||||||
load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
|
load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
|
||||||
baseline: (date) => date.toISOString(),
|
baseline: (date) => date.toISOString(),
|
||||||
@@ -42,6 +45,7 @@ describe("SystemContext", () => {
|
|||||||
const context = SystemContext.combine([
|
const context = SystemContext.combine([
|
||||||
SystemContext.make({
|
SystemContext.make({
|
||||||
key: key("core/date"),
|
key: key("core/date"),
|
||||||
|
description: "Current date",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: Effect.sync(() => {
|
load: Effect.sync(() => {
|
||||||
loads++
|
loads++
|
||||||
@@ -57,7 +61,7 @@ describe("SystemContext", () => {
|
|||||||
expect(yield* SystemContext.initialize(context)).toEqual({
|
expect(yield* SystemContext.initialize(context)).toEqual({
|
||||||
text: "Today's date is 2026-06-03.\n\nDirectory: /repo",
|
text: "Today's date is 2026-06-03.\n\nDirectory: /repo",
|
||||||
applied: {
|
applied: {
|
||||||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
"core/date": { value: "2026-06-03", description: "Current date", removed: "The date was removed." },
|
||||||
"core/location": { value: "/repo" },
|
"core/location": { value: "/repo" },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -84,8 +88,13 @@ describe("SystemContext", () => {
|
|||||||
expect(yield* SystemContext.reconcile(changed, previous)).toEqual({
|
expect(yield* SystemContext.reconcile(changed, previous)).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "The date changed from 2026-06-03 to 2026-06-04.",
|
text: "The date changed from 2026-06-03 to 2026-06-04.",
|
||||||
|
updates: [{ key: key("core/date"), description: "Description for core/date", action: "updated" }],
|
||||||
applied: {
|
applied: {
|
||||||
"core/date": { value: "2026-06-04", removed: "The date was removed." },
|
"core/date": {
|
||||||
|
value: "2026-06-04",
|
||||||
|
description: "Description for core/date",
|
||||||
|
removed: "The date was removed.",
|
||||||
|
},
|
||||||
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -113,6 +122,7 @@ describe("SystemContext", () => {
|
|||||||
expect(yield* SystemContext.reconcile(context, {})).toEqual({
|
expect(yield* SystemContext.reconcile(context, {})).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "Available skill: effect",
|
text: "Available skill: effect",
|
||||||
|
updates: [{ key: key("core/skills"), description: "Description for core/skills", action: "added" }],
|
||||||
applied: { "core/skills": { value: "effect" } },
|
applied: { "core/skills": { value: "effect" } },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -150,6 +160,7 @@ describe("SystemContext", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "Instructions removed; stop applying them.",
|
text: "Instructions removed; stop applying them.",
|
||||||
|
updates: [{ key: key("core/instructions"), description: "core/instructions", action: "removed" }],
|
||||||
applied: {},
|
applied: {},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -169,6 +180,7 @@ describe("SystemContext", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "effect",
|
text: "effect",
|
||||||
|
updates: [{ key: key("core/skills"), description: "Description for core/skills", action: "added" }],
|
||||||
applied: {
|
applied: {
|
||||||
"core/skills": { value: "effect" },
|
"core/skills": { value: "effect" },
|
||||||
"core/date": { value: "2026-06-04" },
|
"core/date": { value: "2026-06-04" },
|
||||||
@@ -208,6 +220,7 @@ describe("SystemContext", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "2026-06-04",
|
text: "2026-06-04",
|
||||||
|
updates: [{ key: key("core/date"), description: "Description for core/date", action: "updated" }],
|
||||||
applied: { "core/date": { value: "2026-06-04" } },
|
applied: { "core/date": { value: "2026-06-04" } },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -232,6 +245,10 @@ describe("SystemContext", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
_tag: "Updated",
|
_tag: "Updated",
|
||||||
text: "2026-06-03 -> 2026-06-04\n\n/repo",
|
text: "2026-06-03 -> 2026-06-04\n\n/repo",
|
||||||
|
updates: [
|
||||||
|
{ key: key("core/date"), description: "Description for core/date", action: "updated" },
|
||||||
|
{ key: key("core/location"), description: "Description for core/location", action: "updated" },
|
||||||
|
],
|
||||||
applied: {
|
applied: {
|
||||||
"core/date": { value: "2026-06-04" },
|
"core/date": { value: "2026-06-04" },
|
||||||
"core/location": { value: "/repo" },
|
"core/location": { value: "/repo" },
|
||||||
@@ -245,6 +262,7 @@ describe("SystemContext", () => {
|
|||||||
let loads = 0
|
let loads = 0
|
||||||
const context = SystemContext.make({
|
const context = SystemContext.make({
|
||||||
key: key("core/date"),
|
key: key("core/date"),
|
||||||
|
description: "Current date",
|
||||||
codec: Schema.toCodecJson(Schema.String),
|
codec: Schema.toCodecJson(Schema.String),
|
||||||
load: Effect.sync(() => {
|
load: Effect.sync(() => {
|
||||||
loads++
|
loads++
|
||||||
@@ -331,6 +349,49 @@ describe("SystemContext", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
expect(
|
||||||
|
SystemContext.diffItems(
|
||||||
|
SystemContext.diffByKey(
|
||||||
|
previous,
|
||||||
|
current,
|
||||||
|
(value) => value.name,
|
||||||
|
(before, after) => before.description !== after.description,
|
||||||
|
),
|
||||||
|
(value) => ({ key: value.name, description: value.description }),
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{ key: "writing", description: "Write prose", action: "added" },
|
||||||
|
{ key: "retired", description: "Old", action: "removed" },
|
||||||
|
{ key: "effect", description: "Build with Effect v4", action: "updated" },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("includes structured item updates from source reconciliation", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const context = stringContext({
|
||||||
|
key: "core/skills",
|
||||||
|
value: "new",
|
||||||
|
description: "Available skills",
|
||||||
|
update: () => ({
|
||||||
|
text: "Skill changes",
|
||||||
|
items: [{ key: "effect", description: "Build with Effect", action: "updated" }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(yield* SystemContext.reconcile(context, { "core/skills": { value: "old" } })).toEqual({
|
||||||
|
_tag: "Updated",
|
||||||
|
text: "Skill changes",
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
key: key("core/skills"),
|
||||||
|
description: "Available skills",
|
||||||
|
action: "updated",
|
||||||
|
items: [{ key: "effect", description: "Build with Effect", action: "updated" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
applied: { "core/skills": { value: "new" } },
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user