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
|
|
@ -196,33 +196,35 @@ export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["param
|
|||
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
export type Endpoint4_19Output = EffectValue<
|
||||
ReturnType<RawClient["server.session"]["session.context.entry.list"]>
|
||||
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
||||
>["data"]
|
||||
export type SessionListContextEntriesOperation<E = never> = (
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint4_19Input,
|
||||
) => Effect.Effect<Endpoint4_19Output, E>
|
||||
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
export type Endpoint4_20Input = {
|
||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_20Request["params"]["key"]
|
||||
readonly value: Endpoint4_20Request["payload"]["value"]
|
||||
}
|
||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
|
||||
export type SessionPutContextEntryOperation<E = never> = (
|
||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint4_20Input,
|
||||
) => Effect.Effect<Endpoint4_20Output, E>
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
export type Endpoint4_21Input = {
|
||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_21Request["params"]["key"]
|
||||
}
|
||||
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>>
|
||||
export type SessionRemoveContextEntryOperation<E = never> = (
|
||||
export type Endpoint4_21Output = EffectValue<
|
||||
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
||||
>
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint4_21Input,
|
||||
) => Effect.Effect<Endpoint4_21Output, E>
|
||||
|
||||
|
|
@ -273,9 +275,13 @@ export interface SessionApi<E = never> {
|
|||
readonly revertClear: SessionRevertClearOperation<E>
|
||||
readonly revertCommit: SessionRevertCommitOperation<E>
|
||||
readonly context: SessionContextOperation<E>
|
||||
readonly listContextEntries: SessionListContextEntriesOperation<E>
|
||||
readonly putContextEntry: SessionPutContextEntryOperation<E>
|
||||
readonly removeContextEntry: SessionRemoveContextEntryOperation<E>
|
||||
readonly instructions: {
|
||||
readonly entry: {
|
||||
readonly list: SessionInstructionsEntryListOperation<E>
|
||||
readonly put: SessionInstructionsEntryPutOperation<E>
|
||||
readonly remove: SessionInstructionsEntryRemoveOperation<E>
|
||||
}
|
||||
}
|
||||
readonly log: SessionLogOperation<E>
|
||||
readonly interrupt: SessionInterruptOperation<E>
|
||||
readonly background: SessionBackgroundOperation<E>
|
||||
|
|
|
|||
|
|
@ -266,33 +266,33 @@ const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18I
|
|||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
type Endpoint4_20Input = {
|
||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_20Request["params"]["key"]
|
||||
readonly value: Endpoint4_20Request["payload"]["value"]
|
||||
}
|
||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||
raw["session.context.entry.put"]({
|
||||
raw["session.instructions.entry.put"]({
|
||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||
payload: { value: input["value"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
type Endpoint4_21Input = {
|
||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_21Request["params"]["key"]
|
||||
}
|
||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
|
|
@ -354,9 +354,7 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
|||
revertClear: Endpoint4_16(raw),
|
||||
revertCommit: Endpoint4_17(raw),
|
||||
context: Endpoint4_18(raw),
|
||||
listContextEntries: Endpoint4_19(raw),
|
||||
putContextEntry: Endpoint4_20(raw),
|
||||
removeContextEntry: Endpoint4_21(raw),
|
||||
instructions: { entry: { list: Endpoint4_19(raw), put: Endpoint4_20(raw), remove: Endpoint4_21(raw) } },
|
||||
log: Endpoint4_22(raw),
|
||||
interrupt: Endpoint4_23(raw),
|
||||
background: Endpoint4_24(raw),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ type PromisifyOperation<Operation> = Operation extends (
|
|||
? (...args: Args) => Promise<Success>
|
||||
: Operation extends (...args: infer Args) => Stream.Stream<infer Success, unknown, unknown>
|
||||
? (...args: Args) => AsyncIterable<Success>
|
||||
: Operation
|
||||
: Operation extends (...args: infer _Args) => unknown
|
||||
? Operation
|
||||
: Operation extends object
|
||||
? PromisifyApi<Operation>
|
||||
: Operation
|
||||
|
||||
type PromisifyApi<Api> = {
|
||||
readonly [Name in keyof Api]: PromisifyOperation<Api[Name]>
|
||||
|
|
|
|||
|
|
@ -43,12 +43,12 @@ import type {
|
|||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionListContextEntriesInput,
|
||||
SessionListContextEntriesOutput,
|
||||
SessionPutContextEntryInput,
|
||||
SessionPutContextEntryOutput,
|
||||
SessionRemoveContextEntryInput,
|
||||
SessionRemoveContextEntryOutput,
|
||||
SessionInstructionsEntryListInput,
|
||||
SessionInstructionsEntryListOutput,
|
||||
SessionInstructionsEntryPutInput,
|
||||
SessionInstructionsEntryPutOutput,
|
||||
SessionInstructionsEntryRemoveInput,
|
||||
SessionInstructionsEntryRemoveOutput,
|
||||
SessionLogInput,
|
||||
SessionLogOutput,
|
||||
SessionInterruptInput,
|
||||
|
|
@ -607,40 +607,44 @@ export function make(options: ClientOptions) {
|
|||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
listContextEntries: (input: SessionListContextEntriesInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionListContextEntriesOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
putContextEntry: (input: SessionPutContextEntryInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPutContextEntryOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry/${encodeURIComponent(input.key)}`,
|
||||
body: { value: input["value"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
removeContextEntry: (input: SessionRemoveContextEntryInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRemoveContextEntryOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry/${encodeURIComponent(input.key)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
instructions: {
|
||||
entry: {
|
||||
list: (input: SessionInstructionsEntryListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionInstructionsEntryListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
put: (input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInstructionsEntryPutOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
|
||||
body: { value: input["value"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
remove: (input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInstructionsEntryRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
},
|
||||
log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable<SessionLogOutput> =>
|
||||
sse<SessionLogOutput>(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1091,26 +1091,26 @@ export type SessionContextOutput = {
|
|||
>
|
||||
}["data"]
|
||||
|
||||
export type SessionListContextEntriesInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionListContextEntriesOutput = {
|
||||
export type SessionInstructionsEntryListOutput = {
|
||||
readonly data: ReadonlyArray<{ readonly key: string; readonly value: JsonValue }>
|
||||
}["data"]
|
||||
|
||||
export type SessionPutContextEntryInput = {
|
||||
export type SessionInstructionsEntryPutInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"]
|
||||
readonly key: { readonly sessionID: string; readonly key: string }["key"]
|
||||
readonly value: { readonly value: JsonValue }["value"]
|
||||
}
|
||||
|
||||
export type SessionPutContextEntryOutput = void
|
||||
export type SessionInstructionsEntryPutOutput = void
|
||||
|
||||
export type SessionRemoveContextEntryInput = {
|
||||
export type SessionInstructionsEntryRemoveInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"]
|
||||
readonly key: { readonly sessionID: string; readonly key: string }["key"]
|
||||
}
|
||||
|
||||
export type SessionRemoveContextEntryOutput = void
|
||||
export type SessionInstructionsEntryRemoveOutput = void
|
||||
|
||||
export type SessionLogInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
|
|
@ -1212,7 +1212,7 @@ export type SessionLogOutput =
|
|||
readonly id: string
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.context.updated"
|
||||
readonly type: "session.instructions.updated"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly sessionID: string; readonly text: string }
|
||||
|
|
@ -4506,7 +4506,7 @@ export type EventSubscribeOutput =
|
|||
readonly id: string
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.context.updated"
|
||||
readonly type: "session.instructions.updated"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly sessionID: string; readonly text: string }
|
||||
|
|
|
|||
|
|
@ -2,9 +2,39 @@ import { Effect } from "effect"
|
|||
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
|
||||
|
||||
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
|
||||
type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
|
||||
|
||||
declare const effectClient: EffectClient
|
||||
declare const promiseClient: PromiseClient
|
||||
|
||||
const effectApi: EffectApi<unknown> = effectClient
|
||||
|
||||
void effectApi
|
||||
declare const sessionID: Parameters<typeof effectApi.session.instructions.entry.list>[0]["sessionID"]
|
||||
|
||||
const effectList: Effect.Effect<
|
||||
ReadonlyArray<{ readonly key: string; readonly value: unknown }>,
|
||||
unknown
|
||||
> = effectApi.session.instructions.entry.list({ sessionID })
|
||||
const effectPut: Effect.Effect<void, unknown> = effectApi.session.instructions.entry.put({
|
||||
sessionID,
|
||||
key: "review-notes",
|
||||
value: { text: "Check the diff" },
|
||||
})
|
||||
const effectRemove: Effect.Effect<void, unknown> = effectApi.session.instructions.entry.remove({
|
||||
sessionID,
|
||||
key: "review-notes",
|
||||
})
|
||||
|
||||
const promiseList: Promise<ReadonlyArray<{ readonly key: string; readonly value: unknown }>> =
|
||||
promiseClient.session.instructions.entry.list({ sessionID: "ses_test" })
|
||||
const promisePut: Promise<void> = promiseClient.session.instructions.entry.put({
|
||||
sessionID: "ses_test",
|
||||
key: "review-notes",
|
||||
value: { text: "Check the diff" },
|
||||
})
|
||||
const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.remove({
|
||||
sessionID: "ses_test",
|
||||
key: "review-notes",
|
||||
})
|
||||
|
||||
void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,57 @@ test("session.get returns the decoded Effect projection", async () => {
|
|||
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
||||
})
|
||||
|
||||
test("session instructions methods use the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string; body?: unknown }> = []
|
||||
const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }]
|
||||
const httpClient = HttpClient.make((request) => {
|
||||
requests.push({
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
body: request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined,
|
||||
})
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
request.method === "GET" ? Response.json({ data: instructions }) : new Response(null, { status: 204 }),
|
||||
),
|
||||
)
|
||||
})
|
||||
const result = await Effect.gen(function* () {
|
||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||
const listed = yield* client.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") })
|
||||
yield* client.session.instructions.entry.put({
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
key: "review-notes",
|
||||
value: instructions[0].value,
|
||||
})
|
||||
yield* client.session.instructions.entry.remove({
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
key: "review-notes",
|
||||
})
|
||||
return listed
|
||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||
|
||||
expect(result).toEqual(instructions)
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
method: "GET",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
|
||||
body: undefined,
|
||||
},
|
||||
{
|
||||
method: "PUT",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
|
||||
body: { value: { text: "Check the diff", priority: 1 } },
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
|
||||
body: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("event.subscribe exposes and decodes the native Effect event stream", async () => {
|
||||
const httpClient = HttpClient.make((request) =>
|
||||
Effect.succeed(
|
||||
|
|
|
|||
|
|
@ -148,6 +148,51 @@ test("session.get returns the wire projection", async () => {
|
|||
expect(result.time.created).toBe(1_717_171_717_000)
|
||||
})
|
||||
|
||||
test("session instructions methods use the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string; body?: unknown }> = []
|
||||
const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }]
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input, init) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
requests.push({
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
body: request.method === "PUT" ? await request.json() : undefined,
|
||||
})
|
||||
if (request.method === "GET") return Response.json({ data: instructions })
|
||||
return new Response(null, { status: 204 })
|
||||
},
|
||||
})
|
||||
|
||||
const result = await client.session.instructions.entry.list({ sessionID: "ses_test" })
|
||||
await client.session.instructions.entry.put({
|
||||
sessionID: "ses_test",
|
||||
key: "review-notes",
|
||||
value: instructions[0].value,
|
||||
})
|
||||
await client.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" })
|
||||
|
||||
expect(result).toEqual(instructions)
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
method: "GET",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries",
|
||||
body: undefined,
|
||||
},
|
||||
{
|
||||
method: "PUT",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
|
||||
body: { value: { text: "Check the diff", priority: 1 } },
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
|
||||
body: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("event.subscribe exposes the Promise event stream wire projection", async () => {
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
|
|
|
|||
|
|
@ -2350,12 +2350,12 @@
|
|||
"summary": "Get session context"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/context-entry": {
|
||||
"/api/session/{sessionID}/instructions/entries": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"operationId": "v2.session.context.entry.list",
|
||||
"operationId": "v2.session.instructions.entry.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
|
|
@ -2383,7 +2383,7 @@
|
|||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionContextEntry.Info"
|
||||
"$ref": "#/components/schemas/InstructionEntry.Info"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -2433,16 +2433,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"description": "List API-managed context entries attached to the session's system context.",
|
||||
"summary": "List context entries"
|
||||
"description": "List API-managed instruction entries attached to the session.",
|
||||
"summary": "List instruction entries"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/context-entry/{key}": {
|
||||
"/api/session/{sessionID}/instructions/entries/{key}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"operationId": "v2.session.context.entry.put",
|
||||
"operationId": "v2.session.instructions.entry.put",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
|
|
@ -2461,7 +2461,7 @@
|
|||
"name": "key",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionContextEntry.Key"
|
||||
"$ref": "#/components/schemas/InstructionEntry.Key"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
|
|
@ -2509,8 +2509,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"description": "Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.",
|
||||
"summary": "Put context entry",
|
||||
"description": "Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.",
|
||||
"summary": "Put instruction entry",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
|
@ -2533,7 +2533,7 @@
|
|||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"operationId": "v2.session.context.entry.remove",
|
||||
"operationId": "v2.session.instructions.entry.remove",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
|
|
@ -2552,7 +2552,7 @@
|
|||
"name": "key",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionContextEntry.Key"
|
||||
"$ref": "#/components/schemas/InstructionEntry.Key"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
|
|
@ -2600,8 +2600,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"description": "Remove one context entry; the removal is announced to the model at the next turn boundary.",
|
||||
"summary": "Remove context entry"
|
||||
"description": "Remove one instruction entry; the removal is announced to the model at the next step boundary.",
|
||||
"summary": "Remove instruction entry"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/log": {
|
||||
|
|
@ -11692,7 +11692,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SessionContextEntry.Key": {
|
||||
"InstructionEntry.Key": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
|
|
@ -11701,11 +11701,11 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SessionContextEntry.Info": {
|
||||
"InstructionEntry.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"$ref": "#/components/schemas/SessionContextEntry.Key"
|
||||
"$ref": "#/components/schemas/InstructionEntry.Key"
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
|
|
@ -12385,7 +12385,7 @@
|
|||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"session.next.context.updated": {
|
||||
"session.next.instructions.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
@ -12402,7 +12402,7 @@
|
|||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.next.context.updated"
|
||||
"session.next.instructions.updated"
|
||||
]
|
||||
},
|
||||
"durable": {
|
||||
|
|
@ -14917,7 +14917,7 @@
|
|||
"$ref": "#/components/schemas/session.next.prompt.admitted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.next.context.updated"
|
||||
"$ref": "#/components/schemas/session.next.instructions.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.next.synthetic"
|
||||
|
|
@ -26060,7 +26060,7 @@
|
|||
"$ref": "#/components/schemas/session.next.execution.settled"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.next.context.updated"
|
||||
"$ref": "#/components/schemas/session.next.instructions.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/session.next.synthetic"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ const json = (value: unknown, status = 200) =>
|
|||
|
||||
const singleOperation = (operation: Record<string, unknown>, method = "get"): Document => ({
|
||||
openapi: "3.1.0",
|
||||
paths: { "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } } },
|
||||
paths: {
|
||||
"/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } },
|
||||
},
|
||||
})
|
||||
|
||||
describe("OpenAPI.fromSpec", () => {
|
||||
|
|
@ -94,7 +96,12 @@ describe("OpenAPI.fromSpec", () => {
|
|||
const remove = toolAt(api.tools, "users.remove")
|
||||
|
||||
expect(api.skipped).toEqual([])
|
||||
if (!Tool.isDefinition(get) || !Tool.isDefinition(create) || !Tool.isDefinition(search) || !Tool.isDefinition(remove)) {
|
||||
if (
|
||||
!Tool.isDefinition(get) ||
|
||||
!Tool.isDefinition(create) ||
|
||||
!Tool.isDefinition(search) ||
|
||||
!Tool.isDefinition(remove)
|
||||
) {
|
||||
throw new Error("happy-path fixture did not generate every operation")
|
||||
}
|
||||
expect(inputTypeScript(get)).toBe(
|
||||
|
|
@ -110,7 +117,8 @@ describe("OpenAPI.fromSpec", () => {
|
|||
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.make({ tools: { api: api.tools } })
|
||||
.execute(`
|
||||
.execute(
|
||||
`
|
||||
const user = await tools.api.users.get({
|
||||
userId: "user-1",
|
||||
include: ["profile", "permissions"],
|
||||
|
|
@ -128,7 +136,8 @@ describe("OpenAPI.fromSpec", () => {
|
|||
})
|
||||
const removed = await tools.api.users.remove({ userId: "user-1" })
|
||||
return { user, created, summary, removed }
|
||||
`)
|
||||
`,
|
||||
)
|
||||
.pipe(Effect.provide(client.layer)),
|
||||
)
|
||||
|
||||
|
|
@ -196,11 +205,11 @@ describe("OpenAPI.fromSpec", () => {
|
|||
if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated")
|
||||
expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }")
|
||||
|
||||
const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put")
|
||||
expect(Tool.isDefinition(contextEntryPut)).toBe(true)
|
||||
if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated")
|
||||
expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
||||
expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined()
|
||||
const instructionPut = toolAt(result.tools, "v2.session.instructions.entry.put")
|
||||
expect(Tool.isDefinition(instructionPut)).toBe(true)
|
||||
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
||||
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
||||
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
||||
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
||||
|
|
@ -481,9 +490,9 @@ describe("OpenAPI.fromSpec", () => {
|
|||
expect(url.searchParams.get("nullable")).toBe("null")
|
||||
expect(url.searchParams.get("constructor")).toBe("safe")
|
||||
expect(client.requests[0]!.headers.meta).toBe("a=b,c=d")
|
||||
await expect(
|
||||
Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer))),
|
||||
).rejects.toThrow("unsupported nested value")
|
||||
await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
|
||||
"unsupported nested value",
|
||||
)
|
||||
})
|
||||
|
||||
test("skips unsupported parameter encodings and malformed security", () => {
|
||||
|
|
@ -585,7 +594,10 @@ describe("OpenAPI.fromSpec", () => {
|
|||
|
||||
test("applies authentication carriers without prototype or collision loss", async () => {
|
||||
const client = recordingClient(() => json({ ok: true }))
|
||||
const authenticated = (security: ReadonlyArray<Record<string, ReadonlyArray<string>>>, schemes: Record<string, unknown>) =>
|
||||
const authenticated = (
|
||||
security: ReadonlyArray<Record<string, ReadonlyArray<string>>>,
|
||||
schemes: Record<string, unknown>,
|
||||
) =>
|
||||
OpenAPI.fromSpec({
|
||||
baseUrl,
|
||||
spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } },
|
||||
|
|
@ -601,13 +613,10 @@ describe("OpenAPI.fromSpec", () => {
|
|||
expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret")
|
||||
|
||||
const duplicate = toolAt(
|
||||
authenticated(
|
||||
[{ first: [], second: [] }],
|
||||
{
|
||||
first: { type: "apiKey", in: "header", name: "x-key" },
|
||||
second: { type: "apiKey", in: "header", name: "x-key" },
|
||||
},
|
||||
).tools,
|
||||
authenticated([{ first: [], second: [] }], {
|
||||
first: { type: "apiKey", in: "header", name: "x-key" },
|
||||
second: { type: "apiKey", in: "header", name: "x-key" },
|
||||
}).tools,
|
||||
"test",
|
||||
)
|
||||
if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated")
|
||||
|
|
@ -632,8 +641,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
},
|
||||
},
|
||||
auth: {
|
||||
resolve: ({ name }) =>
|
||||
Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined),
|
||||
resolve: ({ name }) => Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined),
|
||||
},
|
||||
})
|
||||
const alternativeTool = toolAt(alternative.tools, "test")
|
||||
|
|
@ -765,9 +773,7 @@ describe("OpenAPI.fromSpec", () => {
|
|||
const oversized = recordingClient(
|
||||
() => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }),
|
||||
)
|
||||
const malformed = recordingClient(
|
||||
() => new Response("{", { headers: { "content-type": "application/json" } }),
|
||||
)
|
||||
const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } }))
|
||||
const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1)))
|
||||
|
||||
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"./effect/layer-node": "./src/effect/layer-node.ts",
|
||||
"./effect/app-node": "./src/effect/app-node.ts",
|
||||
"./session/runner": "./src/session/runner/index.ts",
|
||||
"./system-context": "./src/system-context/index.ts",
|
||||
"./instructions": "./src/instructions/index.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"imports": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "96e9fe64-d810-4102-8f79-3317a88bb6d2",
|
||||
"id": "992b24b9-f3e9-41f5-87a5-4917d1423169",
|
||||
"prevIds": [
|
||||
"22e57fed-b9b8-4e94-a3b4-f94bece680a8"
|
||||
"96e9fe64-660f-4a73-9414-b38bb7eac290"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
|
|
@ -50,6 +50,14 @@
|
|||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "instruction_checkpoint",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "instruction_entry",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
|
|
@ -58,14 +66,6 @@
|
|||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_context_epoch",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_context_entry",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_input",
|
||||
"entityType": "tables"
|
||||
|
|
@ -786,6 +786,96 @@
|
|||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_checkpoint"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "baseline",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_checkpoint"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "snapshot",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_checkpoint"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "baseline_seq",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_checkpoint"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "key",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "value",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
|
|
@ -896,96 +986,6 @@
|
|||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "baseline",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "snapshot",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "baseline_seq",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "key",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "value",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
|
|
@ -1621,6 +1621,36 @@
|
|||
"entityType": "fks",
|
||||
"table": "project_directory"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_instruction_checkpoint_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "instruction_checkpoint"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_instruction_entry_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
|
|
@ -1651,36 +1681,6 @@
|
|||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_context_epoch_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_context_entry_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
|
|
@ -1782,9 +1782,9 @@
|
|||
"key"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_context_entry_pk",
|
||||
"name": "instruction_entry_pk",
|
||||
"entityType": "pks",
|
||||
"table": "session_context_entry"
|
||||
"table": "instruction_entry"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
|
|
@ -1877,6 +1877,15 @@
|
|||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "instruction_checkpoint_pk",
|
||||
"table": "instruction_checkpoint",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
|
|
@ -1895,15 +1904,6 @@
|
|||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_context_epoch_pk",
|
||||
"table": "session_context_epoch",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
|
|
@ -2252,4 +2252,4 @@
|
|||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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>) => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migrat
|
|||
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
|
||||
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
|
||||
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
|
||||
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
|
@ -73,11 +74,11 @@ describe("DatabaseMigration", () => {
|
|||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input'`),
|
||||
).toEqual({ name: "session_input" })
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_context_epoch'`),
|
||||
).toEqual({ name: "session_context_epoch" })
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'instruction_checkpoint'`),
|
||||
).toEqual({ name: "instruction_checkpoint" })
|
||||
expect(
|
||||
yield* db.get(
|
||||
sql`SELECT name FROM pragma_table_info('session_context_epoch') WHERE name IN ('agent', 'replacement_seq', 'revision')`,
|
||||
sql`SELECT name FROM pragma_table_info('instruction_checkpoint') WHERE name IN ('agent', 'replacement_seq', 'revision')`,
|
||||
),
|
||||
).toBeUndefined()
|
||||
expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: migrations.length })
|
||||
|
|
@ -131,6 +132,42 @@ describe("DatabaseMigration", () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("renames instruction state without losing rows or durable updates", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_context_entry (session_id text NOT NULL, key text NOT NULL, value text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, PRIMARY KEY(session_id, key))`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_context_epoch (session_id text PRIMARY KEY, baseline text NOT NULL, snapshot text NOT NULL, baseline_seq integer NOT NULL)`,
|
||||
)
|
||||
yield* db.run(sql`CREATE TABLE event (type text NOT NULL)`)
|
||||
yield* db.run(sql`INSERT INTO session_context_entry VALUES ('ses_test', 'plan', '"ready"', 1, 2)`)
|
||||
yield* db.run(sql`INSERT INTO session_context_epoch VALUES ('ses_test', 'baseline', '{}', 7)`)
|
||||
yield* db.run(sql`INSERT INTO event VALUES ('session.context.updated.1')`)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [renameInstructionsMigration])
|
||||
|
||||
expect(yield* db.get(sql`SELECT * FROM instruction_entry`)).toEqual({
|
||||
session_id: "ses_test",
|
||||
key: "plan",
|
||||
value: '"ready"',
|
||||
time_created: 1,
|
||||
time_updated: 2,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT * FROM instruction_checkpoint`)).toEqual({
|
||||
session_id: "ses_test",
|
||||
baseline: "baseline",
|
||||
snapshot: "{}",
|
||||
baseline_seq: 7,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT type FROM event`)).toEqual({ type: "session.instructions.updated.1" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps legacy credential fields nullable", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -264,10 +301,12 @@ describe("DatabaseMigration", () => {
|
|||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||
sql`INSERT INTO instruction_checkpoint (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9)`,
|
||||
)
|
||||
yield* db.run(sql`ALTER TABLE instruction_checkpoint RENAME TO session_context_epoch`)
|
||||
yield* db.run(sql`DELETE FROM migration WHERE id = ${simplifySessionInputMigration.id}`)
|
||||
yield* DatabaseMigration.applyOnly(db, [simplifySessionInputMigration])
|
||||
yield* db.run(sql`ALTER TABLE session_context_epoch RENAME TO instruction_checkpoint`)
|
||||
|
||||
const database = Layer.succeed(Database.Service, { db })
|
||||
yield* EventV2.Service.use((service) =>
|
||||
|
|
@ -299,7 +338,7 @@ describe("DatabaseMigration", () => {
|
|||
(SELECT COUNT(*) FROM workspace) AS workspaces,
|
||||
(SELECT COUNT(*) FROM session_input) AS sessionInputs,
|
||||
(SELECT COUNT(*) FROM session_message) AS sessionMessages,
|
||||
(SELECT COUNT(*) FROM session_context_epoch) AS contextEpochs,
|
||||
(SELECT COUNT(*) FROM instruction_checkpoint) AS instructionCheckpoints,
|
||||
(SELECT seq FROM event_sequence WHERE aggregate_id = 'session') AS seq,
|
||||
(SELECT type FROM event WHERE aggregate_id = 'session') AS eventType
|
||||
`),
|
||||
|
|
@ -311,7 +350,7 @@ describe("DatabaseMigration", () => {
|
|||
workspaces: 0,
|
||||
sessionInputs: 0,
|
||||
sessionMessages: 0,
|
||||
contextEpochs: 0,
|
||||
instructionCheckpoints: 0,
|
||||
seq: 0,
|
||||
eventType: "session.updated.1",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -712,8 +712,8 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const aggregateID = Session.ID.create()
|
||||
const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
|
||||
yield* events.project(SessionEvent.ContextUpdated, (event) =>
|
||||
const received = new Array<typeof SessionEvent.InstructionsUpdated.Type>()
|
||||
yield* events.project(SessionEvent.InstructionsUpdated, (event) =>
|
||||
Effect.sync(() => {
|
||||
received.push(event)
|
||||
}),
|
||||
|
|
@ -722,7 +722,7 @@ describe("EventV2", () => {
|
|||
yield* events.replay({
|
||||
id: EventV2.ID.create(),
|
||||
created: DateTime.makeUnsafe(0),
|
||||
type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
|
||||
type: EventV2.versionedType(SessionEvent.InstructionsUpdated.type, 1),
|
||||
seq: 0,
|
||||
aggregateID,
|
||||
data: { sessionID: aggregateID, text: "context" },
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { location } from "./fixture/location"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
|
@ -21,13 +21,13 @@ const instructionLayer = (input: {
|
|||
locationServiceLayer: Layer.Layer<Location.Service>
|
||||
filesystemLayer?: Layer.Layer<FSUtil.Service>
|
||||
}) =>
|
||||
AppNodeBuilder.build(InstructionContext.node, [
|
||||
AppNodeBuilder.build(InstructionDiscovery.node, [
|
||||
[Global.node, Global.layerWith({ config: input.config })],
|
||||
[Location.node, input.locationServiceLayer],
|
||||
...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []),
|
||||
])
|
||||
|
||||
describe("InstructionContext", () => {
|
||||
describe("InstructionDiscovery", () => {
|
||||
it.live("loads global and upward project AGENTS.md files as one aggregate context", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
@ -51,7 +51,7 @@ describe("InstructionContext", () => {
|
|||
await fs.writeFile(packageFile, "package")
|
||||
})
|
||||
|
||||
const load = InstructionContext.Service.pipe(
|
||||
const load = InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -69,7 +69,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
)
|
||||
|
||||
const initialized = yield* SystemContext.initialize(yield* load)
|
||||
const initialized = yield* Instructions.initialize(yield* load)
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
`Instructions from: ${globalFile}\nglobal`,
|
||||
|
|
@ -80,13 +80,13 @@ describe("InstructionContext", () => {
|
|||
expect(initialized.text).not.toContain("outside")
|
||||
|
||||
yield* Effect.promise(() => fs.writeFile(packageFile, "changed"))
|
||||
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toMatchObject({
|
||||
expect(yield* Instructions.reconcile(yield* load, initialized.applied)).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: expect.stringContaining(`Instructions from: ${packageFile}\nchanged`),
|
||||
})
|
||||
|
||||
yield* Effect.promise(() => fs.rm(packageFile))
|
||||
const partial = yield* SystemContext.reconcile(yield* load, initialized.applied)
|
||||
const partial = yield* Instructions.reconcile(yield* load, initialized.applied)
|
||||
expect(partial).toEqual({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -98,7 +98,7 @@ describe("InstructionContext", () => {
|
|||
})
|
||||
|
||||
yield* Effect.promise(() => Promise.all([fs.rm(globalFile), fs.rm(projectFile)]))
|
||||
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toEqual({
|
||||
expect(yield* Instructions.reconcile(yield* load, initialized.applied)).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Previously loaded instructions no longer apply.",
|
||||
applied: {},
|
||||
|
|
@ -117,7 +117,7 @@ describe("InstructionContext", () => {
|
|||
Effect.gen(function* () {
|
||||
const file = path.join(tmp.path, "AGENTS.md")
|
||||
yield* Effect.promise(() => fs.writeFile(file, ""))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -130,7 +130,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
)
|
||||
|
||||
expect((yield* SystemContext.initialize(context)).text).toBe(`Instructions from: ${file}\n`)
|
||||
expect((yield* Instructions.initialize(context)).text).toBe(`Instructions from: ${file}\n`)
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -146,7 +146,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -161,7 +161,7 @@ describe("InstructionContext", () => {
|
|||
)
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/instructions": {
|
||||
value: [{ path: "/repo/AGENTS.md", content: "old" }],
|
||||
removed: "Previously loaded instructions no longer apply.",
|
||||
|
|
@ -186,7 +186,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const context = yield* InstructionContext.Service.pipe(
|
||||
const context = yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -201,7 +201,7 @@ describe("InstructionContext", () => {
|
|||
)
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/instructions": {
|
||||
value: [{ path: file, content: "old" }],
|
||||
removed: "Previously loaded instructions no longer apply.",
|
||||
|
|
@ -230,7 +230,7 @@ describe("InstructionContext", () => {
|
|||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -260,7 +260,7 @@ describe("InstructionContext", () => {
|
|||
let scanned = false
|
||||
process.env.OPENCODE_DISABLE_PROJECT_CONFIG = "1"
|
||||
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
|
|
@ -292,7 +292,7 @@ describe("InstructionContext", () => {
|
|||
it.effect("does not discover project instructions outside the canonical project root", () =>
|
||||
Effect.gen(function* () {
|
||||
let scanned = false
|
||||
yield* InstructionContext.Service.pipe(
|
||||
yield* InstructionDiscovery.Service.pipe(
|
||||
Effect.flatMap((service) => service.load()),
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
83
packages/core/test/instructions/builtins.test.ts
Normal file
83
packages/core/test/instructions/builtins.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
|
||||
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/repo"))
|
||||
const timestamp = Date.parse("2026-06-03T12:00:00.000Z")
|
||||
const localDate = (time: number) => new Date(time).toDateString()
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory },
|
||||
{ projectDirectory, vcs: { type: "git", store: AbsolutePath.make(FSUtil.resolve("/repo/.git")) } },
|
||||
),
|
||||
),
|
||||
)
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(InstructionBuiltIns.node, [
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
|
||||
describe("InstructionBuiltIns", () => {
|
||||
it.effect("loads location-scoped environment and host-local date instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
"Here is some useful information about the environment you are running in:",
|
||||
"<env>",
|
||||
` Working directory: ${directory}`,
|
||||
` Workspace root folder: ${projectDirectory}`,
|
||||
" Is directory a git repo: yes",
|
||||
` Platform: ${process.platform}`,
|
||||
"</env>",
|
||||
"",
|
||||
`Today's date: ${localDate(timestamp)}`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles the date without repeating unchanged environment instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
|
||||
const refreshed = yield* Instructions.reconcile(yield* context.load(), initialized.applied)
|
||||
|
||||
expect(refreshed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not update again within the same local calendar day", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
|
||||
expect(yield* Instructions.reconcile(yield* context.load(), initialized.applied)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const key = SystemContext.Key.make
|
||||
const key = Instructions.Key.make
|
||||
const stringContext = (input: {
|
||||
key: string
|
||||
value: string | SystemContext.Unavailable
|
||||
value: string | Instructions.Unavailable
|
||||
baseline?: (value: string) => string
|
||||
update?: (previous: string, current: string) => string
|
||||
removed?: (value: string) => string
|
||||
}) =>
|
||||
SystemContext.make({
|
||||
Instructions.make({
|
||||
key: key(input.key),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.succeed(input.value),
|
||||
|
|
@ -20,10 +20,10 @@ const stringContext = (input: {
|
|||
removed: input.removed,
|
||||
})
|
||||
|
||||
describe("SystemContext", () => {
|
||||
describe("Instructions", () => {
|
||||
it.effect("stores the canonical JSON encoding of the loaded value", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.make({
|
||||
const context = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.DateFromString),
|
||||
load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
|
||||
|
|
@ -32,15 +32,15 @@ describe("SystemContext", () => {
|
|||
removed: () => "Date removed",
|
||||
})
|
||||
|
||||
expect((yield* SystemContext.initialize(context)).applied["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
|
||||
expect((yield* Instructions.initialize(context)).applied["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("loads once and initializes a baseline with the applied values", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = SystemContext.combine([
|
||||
SystemContext.make({
|
||||
const context = Instructions.combine([
|
||||
Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
|
|
@ -54,7 +54,7 @@ describe("SystemContext", () => {
|
|||
stringContext({ key: "core/location", value: "/repo", baseline: (value) => `Directory: ${value}` }),
|
||||
])
|
||||
|
||||
expect(yield* SystemContext.initialize(context)).toEqual({
|
||||
expect(yield* Instructions.initialize(context)).toEqual({
|
||||
text: "Today's date is 2026-06-03.\n\nDirectory: /repo",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
|
|
@ -71,7 +71,7 @@ describe("SystemContext", () => {
|
|||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
||||
}
|
||||
const changed = SystemContext.combine([
|
||||
const changed = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
|
|
@ -81,7 +81,7 @@ describe("SystemContext", () => {
|
|||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
])
|
||||
|
||||
expect(yield* SystemContext.reconcile(changed, previous)).toEqual({
|
||||
expect(yield* Instructions.reconcile(changed, previous)).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "The date changed from 2026-06-03 to 2026-06-04.",
|
||||
applied: {
|
||||
|
|
@ -91,8 +91,8 @@ describe("SystemContext", () => {
|
|||
})
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(
|
||||
SystemContext.combine([
|
||||
yield* Instructions.reconcile(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-03", removed: () => "The date was removed." }),
|
||||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
]),
|
||||
|
|
@ -110,7 +110,7 @@ describe("SystemContext", () => {
|
|||
baseline: (skill) => `Available skill: ${skill}`,
|
||||
})
|
||||
|
||||
expect(yield* SystemContext.reconcile(context, {})).toEqual({
|
||||
expect(yield* Instructions.reconcile(context, {})).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Available skill: effect",
|
||||
applied: { "core/skills": { value: "effect" } },
|
||||
|
|
@ -121,30 +121,28 @@ describe("SystemContext", () => {
|
|||
it.effect("retains the belief while a source is temporarily unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const previous = { "core/remote": { value: "instructions", removed: "Instructions removed" } }
|
||||
const context = stringContext({ key: "core/remote", value: SystemContext.unavailable })
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
expect(yield* SystemContext.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
|
||||
expect(yield* Instructions.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("blocks initialization while a source is unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* SystemContext.initialize(
|
||||
stringContext({ key: "core/remote", value: SystemContext.unavailable }),
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/remote", value: Instructions.unavailable }),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit))
|
||||
expect(Cause.squash(exit.cause)).toEqual(
|
||||
new SystemContext.InitializationBlocked({ keys: [key("core/remote")] }),
|
||||
)
|
||||
expect(Cause.squash(exit.cause)).toEqual(new Instructions.InitializationBlocked({ keys: [key("core/remote")] }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("emits the previously stored removal message", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(SystemContext.empty, {
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/instructions": { value: "contents", removed: "Instructions removed; stop applying them." },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -157,13 +155,13 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("retains an unannounced removal silently", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(yield* SystemContext.reconcile(SystemContext.empty, { "core/date": { value: "2026-06-04" } })).toEqual({
|
||||
expect(yield* Instructions.reconcile(Instructions.empty, { "core/date": { value: "2026-06-04" } })).toEqual({
|
||||
_tag: "Unchanged",
|
||||
})
|
||||
|
||||
// The retained belief survives alongside other updates.
|
||||
expect(
|
||||
yield* SystemContext.reconcile(stringContext({ key: "core/skills", value: "effect" }), {
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/skills", value: "effect" }), {
|
||||
"core/date": { value: "2026-06-04" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -180,7 +178,7 @@ describe("SystemContext", () => {
|
|||
it.effect("renders multiple removals in stable key order", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(SystemContext.empty, {
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/z": { value: "z", removed: "Removed z" },
|
||||
"core/a": { value: "a", removed: "Removed a" },
|
||||
}),
|
||||
|
|
@ -190,7 +188,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("rejects empty model-visible renderings", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* SystemContext.initialize(
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/empty", value: "value", baseline: () => "" }),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
|
|
@ -202,7 +200,7 @@ describe("SystemContext", () => {
|
|||
it.effect("re-announces the baseline when a stored value no longer decodes", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* SystemContext.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
|
||||
"core/date": { value: 42, removed: "Date removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -215,7 +213,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("renders undecodable re-announcements alongside other updates", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.combine([
|
||||
const context = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
|
|
@ -225,7 +223,7 @@ describe("SystemContext", () => {
|
|||
])
|
||||
|
||||
expect(
|
||||
yield* SystemContext.reconcile(context, {
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/date": { value: "2026-06-03" },
|
||||
"core/location": { value: 42 },
|
||||
}),
|
||||
|
|
@ -243,7 +241,7 @@ describe("SystemContext", () => {
|
|||
it.effect("rebaselines from one coherent source observation", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = SystemContext.make({
|
||||
const context = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
|
|
@ -254,7 +252,7 @@ describe("SystemContext", () => {
|
|||
update: (_previous, current) => current,
|
||||
})
|
||||
|
||||
expect(yield* SystemContext.rebaseline(context, { "core/date": { value: "2026-06-03" } })).toEqual({
|
||||
expect(yield* Instructions.rebaseline(context, { "core/date": { value: "2026-06-03" } })).toEqual({
|
||||
text: "2026-06-04",
|
||||
applied: { "core/date": { value: "2026-06-04" } },
|
||||
})
|
||||
|
|
@ -264,17 +262,17 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("rebaselines an unavailable source from the last-applied belief", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = SystemContext.combine([
|
||||
const context = Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-04" }),
|
||||
stringContext({
|
||||
key: "core/remote",
|
||||
value: SystemContext.unavailable,
|
||||
value: Instructions.unavailable,
|
||||
baseline: (value) => `Instructions: ${value}`,
|
||||
}),
|
||||
])
|
||||
|
||||
expect(
|
||||
yield* SystemContext.rebaseline(context, {
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: "contents", removed: "Instructions removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -289,11 +287,11 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("drops undecodable beliefs and removed sources at rebaseline", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = stringContext({ key: "core/remote", value: SystemContext.unavailable })
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
// Undecodable belief cannot be restated; removed source entries self-clean.
|
||||
expect(
|
||||
yield* SystemContext.rebaseline(context, {
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: 42 },
|
||||
"core/gone": { value: "gone" },
|
||||
}),
|
||||
|
|
@ -315,7 +313,7 @@ describe("SystemContext", () => {
|
|||
]
|
||||
|
||||
expect(
|
||||
SystemContext.diffByKey(
|
||||
Instructions.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(value) => value.name,
|
||||
|
|
@ -337,19 +335,19 @@ describe("SystemContext", () => {
|
|||
it.effect("rejects duplicate source keys", () =>
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
SystemContext.combine([
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "one" }),
|
||||
stringContext({ key: "core/date", value: "two" }),
|
||||
]),
|
||||
).toThrow(new SystemContext.DuplicateKeyError({ key: key("core/date") }))
|
||||
).toThrow(new Instructions.DuplicateKeyError({ key: key("core/date") }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("combines contexts in order", () =>
|
||||
it.effect("combines instructions in order", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
(yield* SystemContext.initialize(
|
||||
SystemContext.combine([
|
||||
(yield* Instructions.initialize(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "date" }),
|
||||
stringContext({ key: "core/location", value: "location" }),
|
||||
]),
|
||||
|
|
@ -360,7 +358,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("requires namespaced source keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeKey = Schema.decodeUnknownSync(SystemContext.Key)
|
||||
const decodeKey = Schema.decodeUnknownSync(Instructions.Key)
|
||||
|
||||
expect(decodeKey("core/date")).toBe(key("core/date"))
|
||||
expect(() => decodeKey("date")).toThrow()
|
||||
|
|
@ -369,7 +367,7 @@ describe("SystemContext", () => {
|
|||
|
||||
it.effect("requires namespaced applied keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeApplied = Schema.decodeUnknownSync(SystemContext.Applied)
|
||||
const decodeApplied = Schema.decodeUnknownSync(Instructions.Applied)
|
||||
|
||||
expect(Object.keys(decodeApplied({ "core/date": { value: "date" } }))).toEqual(["core/date"])
|
||||
expect(() => decodeApplied({ date: { value: "date" } })).toThrow()
|
||||
|
|
@ -4,17 +4,17 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
const guidanceLayer = (referenceLayer: Layer.Layer<Reference.Service>) =>
|
||||
AppNodeBuilder.build(ReferenceGuidance.node, [[Reference.node, referenceLayer]])
|
||||
|
||||
describe("ReferenceGuidance", () => {
|
||||
it.effect("lists available references in the system context", () =>
|
||||
it.effect("lists available references in the instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
|
||||
expect(generation.text).toContain("<available_references>")
|
||||
expect(generation.text).toContain("<name>docs</name>")
|
||||
|
|
@ -46,7 +46,7 @@ describe("ReferenceGuidance", () => {
|
|||
it.effect("omits guidance when no references are available", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
expect(generation.text).toBe("")
|
||||
}).pipe(Effect.provide(guidanceLayer(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })))),
|
||||
)
|
||||
|
|
@ -54,7 +54,7 @@ describe("ReferenceGuidance", () => {
|
|||
it.effect("omits references without descriptions", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const generation = yield* Instructions.initialize(yield* guidance.load())
|
||||
expect(generation.text).toBe("")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
|
|
@ -85,10 +85,10 @@ describe("ReferenceGuidance", () => {
|
|||
let references = [reference("docs", "Use for product documentation")]
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* guidance.load())
|
||||
const initialized = yield* Instructions.initialize(yield* guidance.load())
|
||||
|
||||
references = [reference("docs", "Use for product documentation"), reference("examples", "Use for examples")]
|
||||
const added = yield* SystemContext.reconcile(yield* guidance.load(), initialized.applied)
|
||||
const added = yield* Instructions.reconcile(yield* guidance.load(), initialized.applied)
|
||||
expect(added).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -103,7 +103,7 @@ describe("ReferenceGuidance", () => {
|
|||
|
||||
references = [reference("examples", "Use for examples")]
|
||||
expect(
|
||||
yield* SystemContext.reconcile(yield* guidance.load(), added._tag === "Updated" ? added.applied : {}),
|
||||
yield* Instructions.reconcile(yield* guidance.load(), added._tag === "Updated" ? added.applied : {}),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: "The following project references are no longer available and must not be used: docs.",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import {
|
||||
SessionContextCheckpointTable,
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
|
|
@ -80,7 +80,7 @@ describe("SessionProjector", () => {
|
|||
])
|
||||
.run()
|
||||
yield* db
|
||||
.insert(SessionContextCheckpointTable)
|
||||
.insert(InstructionCheckpointTable)
|
||||
.values({ session_id: sessionID, baseline: "baseline", snapshot: {}, baseline_seq: 0 })
|
||||
.run()
|
||||
const events = yield* EventV2.Service
|
||||
|
|
@ -107,7 +107,7 @@ describe("SessionProjector", () => {
|
|||
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
|
||||
).toEqual([earlier])
|
||||
// A committed revert resets the context checkpoint so the next turn re-initializes.
|
||||
expect(yield* db.select().from(SessionContextCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||
expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
|||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
|
|
@ -73,18 +73,18 @@ const model = OpenAIChat.route
|
|||
})
|
||||
.model({ id: "gpt-4o-mini" })
|
||||
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
|
||||
const systemContext = Layer.mock(SystemContextBuiltIns.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[LayerNodePlatform.llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -119,8 +119,8 @@ const it = testEffect(
|
|||
AgentV2.node,
|
||||
ToolRegistry.node,
|
||||
SessionRunnerModel.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
Config.node,
|
||||
|
|
@ -133,8 +133,8 @@ const it = testEffect(
|
|||
[PermissionV2.node, permission],
|
||||
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@ import { Config } from "@opencode-ai/core/config"
|
|||
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||
import {
|
||||
SessionContextCheckpointTable,
|
||||
InstructionCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
|
|
@ -180,24 +180,24 @@ const models = SessionRunnerModel.layerWith((session) =>
|
|||
),
|
||||
),
|
||||
)
|
||||
const systemContextKey = SystemContext.Key.make("test/context")
|
||||
const systemContextKey = Instructions.Key.make("test/context")
|
||||
let systemBaseline = "Initial context"
|
||||
let systemRemoved = false
|
||||
let systemUnavailable = false
|
||||
let systemLoadHook = Effect.void
|
||||
const skillBaselines = new Map<AgentV2.ID, string>()
|
||||
const systemContext = Layer.mock(SystemContextBuiltIns.Service, {
|
||||
const systemContext = Layer.mock(InstructionBuiltIns.Service, {
|
||||
load: () =>
|
||||
Effect.sync(() =>
|
||||
SystemContext.combine(
|
||||
Instructions.combine(
|
||||
systemRemoved
|
||||
? []
|
||||
: [
|
||||
SystemContext.make({
|
||||
Instructions.make({
|
||||
key: systemContextKey,
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: systemLoadHook.pipe(
|
||||
Effect.andThen(Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline))),
|
||||
Effect.andThen(Effect.sync(() => (systemUnavailable ? Instructions.unavailable : systemBaseline))),
|
||||
),
|
||||
baseline: String,
|
||||
update: (_previous, current) => current,
|
||||
|
|
@ -207,24 +207,24 @@ const systemContext = Layer.mock(SystemContextBuiltIns.Service, {
|
|||
),
|
||||
),
|
||||
})
|
||||
const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
||||
load: (agent) =>
|
||||
Effect.succeed(
|
||||
skillBaselines.has(agent.id)
|
||||
? SystemContext.make({
|
||||
key: SystemContext.Key.make("test/skill-guidance"),
|
||||
? Instructions.make({
|
||||
key: Instructions.Key.make("test/skill-guidance"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.succeed(skillBaselines.get(agent.id)!),
|
||||
baseline: String,
|
||||
update: (_previous, current) => current,
|
||||
removed: () => "Skill guidance removed",
|
||||
})
|
||||
: SystemContext.empty,
|
||||
: Instructions.empty,
|
||||
),
|
||||
})
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
|
|
@ -246,8 +246,8 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
|||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[LayerNodePlatform.llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -285,9 +285,9 @@ const it = testEffect(
|
|||
ToolRegistry.toolsNode,
|
||||
echoNode,
|
||||
SessionRunnerModel.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
SessionContextEntry.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
InstructionEntry.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
Config.node,
|
||||
|
|
@ -300,8 +300,8 @@ const it = testEffect(
|
|||
[LayerNodePlatform.llmClient, client],
|
||||
[PermissionV2.node, permission],
|
||||
[SessionRunnerModel.node, models],
|
||||
[SystemContextBuiltIns.node, systemContext],
|
||||
[InstructionContext.node, instructionContext],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
[InstructionDiscovery.node, instructionContext],
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillGuidance.node, skillGuidance],
|
||||
[ReferenceGuidance.node, referenceGuidance],
|
||||
|
|
@ -709,14 +709,14 @@ describe("SessionRunnerLLM", () => {
|
|||
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(SystemContext.InitializationBlocked)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Instructions.InitializationBlocked)
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(yield* SessionInput.hasPending(db, sessionID, "steer")).toBe(true)
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get(),
|
||||
).toBeUndefined()
|
||||
|
||||
|
|
@ -747,8 +747,8 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get(),
|
||||
).toBeUndefined()
|
||||
|
||||
|
|
@ -774,16 +774,16 @@ describe("SessionRunnerLLM", () => {
|
|||
|
||||
const parent = yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
expect(parent).toBeDefined()
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, forked.id))
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, forked.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie),
|
||||
).toEqual({ ...parent!, session_id: forked.id })
|
||||
|
|
@ -799,9 +799,9 @@ describe("SessionRunnerLLM", () => {
|
|||
response = []
|
||||
yield* session.resume(sessionID)
|
||||
yield* db
|
||||
.update(SessionContextCheckpointTable)
|
||||
.update(InstructionCheckpointTable)
|
||||
.set({ snapshot: { invalid: { value: "bad" } } })
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
|
||||
|
|
@ -815,9 +815,9 @@ describe("SessionRunnerLLM", () => {
|
|||
expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
|
||||
expect(requests[0]?.messages.at(1)?.content).toEqual([{ type: "text", text: "Initial context" }])
|
||||
const healed = yield* db
|
||||
.select({ snapshot: SessionContextCheckpointTable.snapshot })
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.select({ snapshot: InstructionCheckpointTable.snapshot })
|
||||
.from(InstructionCheckpointTable)
|
||||
.where(eq(InstructionCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
expect(healed?.snapshot).toEqual({ "test/context": { value: "Initial context", removed: expect.any(String) } })
|
||||
|
|
@ -849,7 +849,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* db
|
||||
.select({ id: EventTable.id })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.type, "session.context.updated.1"))
|
||||
.where(eq(EventTable.type, "session.instructions.updated.1"))
|
||||
.all()
|
||||
.pipe(Effect.orDie),
|
||||
).toHaveLength(1)
|
||||
|
|
@ -1109,7 +1109,7 @@ describe("SessionRunnerLLM", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
const contextEntries = yield* InstructionEntry.Service
|
||||
yield* contextEntries.put({ sessionID, key: "deploy-target", value: "production" })
|
||||
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
|||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
|
|
@ -71,7 +71,7 @@ describe("SkillGuidance", () => {
|
|||
expect(
|
||||
yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied))),
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: "The following skills are no longer available and must not be used: effect.",
|
||||
|
|
@ -92,12 +92,12 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
skills = [effect, debugging]
|
||||
const added = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied)))
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied)))
|
||||
expect(added).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: [
|
||||
|
|
@ -113,7 +113,7 @@ describe("SkillGuidance", () => {
|
|||
const removed = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(
|
||||
Effect.flatMap((context) => SystemContext.reconcile(context, added._tag === "Updated" ? added.applied : {})),
|
||||
Effect.flatMap((context) => Instructions.reconcile(context, added._tag === "Updated" ? added.applied : {})),
|
||||
)
|
||||
expect(removed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
|
|
@ -129,13 +129,13 @@ describe("SkillGuidance", () => {
|
|||
const guidance = yield* SkillGuidance.Service
|
||||
const initialized = yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
.pipe(Effect.flatMap(Instructions.initialize))
|
||||
|
||||
skills = [SkillV2.Info.make({ ...effect, description: "Build applications with Effect v4" })]
|
||||
expect(
|
||||
yield* guidance
|
||||
.load({ id: agent.id, info: agent })
|
||||
.pipe(Effect.flatMap((context) => SystemContext.reconcile(context, initialized.applied))),
|
||||
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))),
|
||||
).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: expect.stringContaining(
|
||||
|
|
@ -152,12 +152,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
||||
|
|
@ -171,12 +171,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ describe("SkillGuidance", () => {
|
|||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize))).text,
|
||||
(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).text,
|
||||
).toContain("<name>effect</name>")
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
|
|
@ -207,12 +207,12 @@ describe("SkillGuidance", () => {
|
|||
})
|
||||
return Effect.gen(function* () {
|
||||
const guidance = yield* SkillGuidance.Service
|
||||
expect(
|
||||
yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(SystemContext.initialize)),
|
||||
).toEqual({
|
||||
text: "",
|
||||
applied: {},
|
||||
})
|
||||
expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual(
|
||||
{
|
||||
text: "",
|
||||
applied: {},
|
||||
},
|
||||
)
|
||||
}).pipe(Effect.provide(layer(() => [effect])))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||
import { InstructionContext } from "@opencode-ai/core/instruction-context"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
|
||||
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/repo"))
|
||||
const instructionFile = FSUtil.resolve("/repo/AGENTS.md")
|
||||
const timestamp = Date.parse("2026-06-03T12:00:00.000Z")
|
||||
const localDate = (time: number) => new Date(time).toDateString()
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory },
|
||||
{ projectDirectory, vcs: { type: "git", store: AbsolutePath.make(FSUtil.resolve("/repo/.git")) } },
|
||||
),
|
||||
),
|
||||
)
|
||||
const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, InstructionContext.node])
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(builtInsNode, [
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
const instructionFS = Layer.effect(
|
||||
FSUtil.Service,
|
||||
FSUtil.Service.pipe(
|
||||
Effect.map((fs) =>
|
||||
FSUtil.Service.of({
|
||||
...fs,
|
||||
up: () => Effect.succeed([instructionFile]),
|
||||
readFileStringSafe: (path) => Effect.succeed(path === instructionFile ? "Be precise." : undefined),
|
||||
}),
|
||||
),
|
||||
),
|
||||
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
||||
const itWithInstructions = testEffect(
|
||||
AppNodeBuilder.build(builtInsNode, [
|
||||
[Location.node, locationLayer],
|
||||
[FSUtil.node, instructionFS],
|
||||
[Global.node, Global.layerWith({ config: "/global" })],
|
||||
]),
|
||||
)
|
||||
|
||||
describe("SystemContextBuiltIns", () => {
|
||||
it.effect("loads location-scoped environment and host-local date context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
"Here is some useful information about the environment you are running in:",
|
||||
"<env>",
|
||||
` Working directory: ${directory}`,
|
||||
` Workspace root folder: ${projectDirectory}`,
|
||||
" Is directory a git repo: yes",
|
||||
` Platform: ${process.platform}`,
|
||||
"</env>",
|
||||
"",
|
||||
`Today's date: ${localDate(timestamp)}`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles the date without repeating unchanged environment context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
|
||||
const refreshed = yield* SystemContext.reconcile(yield* context.load(), initialized.applied)
|
||||
|
||||
expect(refreshed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not update again within the same local calendar day", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* SystemContextBuiltIns.Service
|
||||
const initialized = yield* SystemContext.initialize(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
|
||||
expect(yield* SystemContext.reconcile(yield* context.load(), initialized.applied)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
|
||||
itWithInstructions.effect("composes ambient instructions after built-in context", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const builtIns = yield* SystemContextBuiltIns.Service
|
||||
const instructions = yield* InstructionContext.Service
|
||||
const context = {
|
||||
load: () => Effect.all([builtIns.load(), instructions.load()]).pipe(Effect.map(SystemContext.combine)),
|
||||
}
|
||||
|
||||
expect((yield* SystemContext.initialize(yield* context.load())).text).toBe(
|
||||
[
|
||||
"Here is some useful information about the environment you are running in:",
|
||||
"<env>",
|
||||
` Working directory: ${directory}`,
|
||||
` Workspace root folder: ${projectDirectory}`,
|
||||
" Is directory a git repo: yes",
|
||||
` Platform: ${process.platform}`,
|
||||
"</env>",
|
||||
"",
|
||||
`Today's date: ${localDate(timestamp)}`,
|
||||
"",
|
||||
`Instructions from: ${instructionFile}\nBe precise.`,
|
||||
].join("\n"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ import {
|
|||
compile as compileContract,
|
||||
emitEffect,
|
||||
emitEffectImported,
|
||||
emitEffectShape,
|
||||
emitPromise,
|
||||
generate,
|
||||
GenerationError,
|
||||
|
|
@ -151,6 +152,201 @@ describe("HttpApiCodegen.generate", () => {
|
|||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.operation.name)).toEqual(["listRequests", "list"])
|
||||
})
|
||||
|
||||
test("supports explicit nested endpoint paths while string aliases remain flat", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("server.session")
|
||||
.add(HttpApiEndpoint.get("session.instructions.list", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.put("session.instructions.put", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.delete("session.instructions.remove", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("session.messages", "/session/message", { success: Schema.String })),
|
||||
)
|
||||
const contract = compileContract(source, {
|
||||
groupNames: { "server.session": "session" },
|
||||
endpointNames: {
|
||||
"session.instructions.list": ["instructions", "list"],
|
||||
"session.instructions.put": ["instructions", "put"],
|
||||
"session.instructions.remove": ["instructions", "remove"],
|
||||
"session.messages": "instructions.flat",
|
||||
},
|
||||
})
|
||||
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.clientPath)).toEqual([
|
||||
["instructions", "list"],
|
||||
["instructions", "put"],
|
||||
["instructions", "remove"],
|
||||
["instructions.flat"],
|
||||
])
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.operation.name)).toEqual([
|
||||
"instructions.list",
|
||||
"instructions.put",
|
||||
"instructions.remove",
|
||||
"instructions.flat",
|
||||
])
|
||||
|
||||
const promise = emitPromise(contract, {
|
||||
outputTypes: {
|
||||
"session.instructions.list": {
|
||||
name: "InstructionListWire",
|
||||
import: 'import type { InstructionListWire } from "./instruction-list-wire"',
|
||||
},
|
||||
},
|
||||
})
|
||||
const promiseClient = promise.files.find((file) => file.path === "client.ts")?.content
|
||||
const promiseTypes = promise.files.find((file) => file.path === "types.ts")?.content
|
||||
expect(promiseClient).toContain('"session": { "instructions": { "list": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"put": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"remove": (requestOptions?: RequestOptions)')
|
||||
expect(promiseClient).toContain('"instructions.flat": (requestOptions?: RequestOptions)')
|
||||
expect(promiseTypes).toContain('import type { InstructionListWire } from "./instruction-list-wire"')
|
||||
expect(promiseTypes).toContain("export type SessionInstructionsListOutput = InstructionListWire")
|
||||
expect(promiseTypes).toContain("export type SessionInstructionsPutOutput = string")
|
||||
expect(promiseTypes).toContain("export type SessionInstructionsRemoveOutput = string")
|
||||
|
||||
const effect = emitEffect(contract)
|
||||
expect(effect.files.find((file) => file.path === "session.ts")?.content).toContain(
|
||||
'"instructions": { "list": Endpoint0(raw), "put": Endpoint1(raw), "remove": Endpoint2(raw) }, "instructions.flat": Endpoint3(raw)',
|
||||
)
|
||||
|
||||
const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" })
|
||||
expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain(
|
||||
'"instructions": { "list": Endpoint0_0(raw), "put": Endpoint0_1(raw), "remove": Endpoint0_2(raw) }, "instructions.flat": Endpoint0_3(raw)',
|
||||
)
|
||||
|
||||
const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" })
|
||||
const apiShape = shape.files.find((file) => file.path === "api.ts")?.content
|
||||
expect(apiShape).toContain('readonly "instructions": { readonly "list": SessionInstructionsListOperation<E>')
|
||||
expect(apiShape).toContain('readonly "put": SessionInstructionsPutOperation<E>')
|
||||
expect(apiShape).toContain('readonly "remove": SessionInstructionsRemoveOperation<E>')
|
||||
expect(apiShape).toContain('readonly "instructions.flat": SessionInstructionsFlatOperation<E>')
|
||||
})
|
||||
|
||||
test("executes nested Promise endpoint aliases", async () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("instructions.list", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.put("instructions.put", "/session/instructions", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.delete("instructions.remove", "/session/instructions", { success: Schema.String })),
|
||||
)
|
||||
const output = emitPromise(
|
||||
compileContract(source, {
|
||||
endpointNames: {
|
||||
"instructions.list": ["instructions", "list"],
|
||||
"instructions.put": ["instructions", "put"],
|
||||
"instructions.remove": ["instructions", "remove"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-httpapi-codegen-"))
|
||||
const methods: Array<string> = []
|
||||
|
||||
try {
|
||||
await Promise.all(output.files.map((file) => Bun.write(join(directory, file.path), file.content)))
|
||||
const generated = await import(`${join(directory, "index.ts")}?t=${crypto.randomUUID()}`)
|
||||
const client = generated.OpenCode.make({
|
||||
baseUrl: "https://example.com",
|
||||
fetch: async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
methods.push(init?.method ?? "GET")
|
||||
return Response.json("ok")
|
||||
},
|
||||
})
|
||||
|
||||
expect(await client.session.instructions.list()).toBe("ok")
|
||||
expect(await client.session.instructions.put()).toBe("ok")
|
||||
expect(await client.session.instructions.remove()).toBe("ok")
|
||||
expect(methods).toEqual(["GET", "PUT", "DELETE"])
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects duplicate and leaf-namespace endpoint paths", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("first", "/first", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })),
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compileContract(source, { endpointNames: { first: ["instructions", "list"], second: ["instructions", "list"] } }),
|
||||
).toThrow("Client endpoint name collision: session.instructions.list")
|
||||
expect(() =>
|
||||
compileContract(source, { endpointNames: { first: "instructions", second: ["instructions", "list"] } }),
|
||||
).toThrow("Client endpoint name collision: session.instructions.list")
|
||||
})
|
||||
|
||||
test("rejects nested root collisions across top-level groups", () => {
|
||||
const source = HttpApi.make("test")
|
||||
.add(
|
||||
HttpApiGroup.make("first", { topLevel: true }).add(
|
||||
HttpApiEndpoint.get("first.list", "/first", { success: Schema.String }),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiGroup.make("second", { topLevel: true }).add(
|
||||
HttpApiEndpoint.get("second.put", "/second", { success: Schema.String }),
|
||||
),
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compileContract(source, {
|
||||
endpointNames: { "first.list": ["instructions", "list"], "second.put": ["instructions", "put"] },
|
||||
}),
|
||||
).toThrow("Client name collision: instructions")
|
||||
})
|
||||
|
||||
test("rejects nested paths that collide after type-name normalization", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(HttpApiEndpoint.get("first", "/first", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })),
|
||||
)
|
||||
|
||||
expect(() => compileContract(source, { endpointNames: { first: ["foo", "bar"], second: "foo-bar" } })).toThrow(
|
||||
"Client endpoint type collision: SessionFooBar",
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects ambiguous and prototype-mutating nested path segments", () => {
|
||||
const source = api(HttpApiEndpoint.get("get", "/session", { success: Schema.String }))
|
||||
|
||||
expect(() => compileContract(source, { endpointNames: { get: ["a.b", "get"] } })).toThrow(
|
||||
"Nested client endpoint path segments cannot contain dots",
|
||||
)
|
||||
expect(() => compileContract(source, { endpointNames: { get: ["__proto__", "get"] } })).toThrow(
|
||||
"Client endpoint path cannot contain __proto__",
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects normalized group, operation-key, and group prototype collisions", () => {
|
||||
const normalized = HttpApi.make("test")
|
||||
.add(HttpApiGroup.make("foo-bar").add(HttpApiEndpoint.get("get", "/first", { success: Schema.String })))
|
||||
.add(HttpApiGroup.make("foo.bar").add(HttpApiEndpoint.get("get", "/second", { success: Schema.String })))
|
||||
expect(() => compileContract(normalized)).toThrow("Client group type collision: FooBar")
|
||||
|
||||
const endpointType = HttpApi.make("test")
|
||||
.add(HttpApiGroup.make("foo").add(HttpApiEndpoint.get("first", "/first", { success: Schema.String })))
|
||||
.add(HttpApiGroup.make("fooBar").add(HttpApiEndpoint.get("second", "/second", { success: Schema.String })))
|
||||
expect(() =>
|
||||
compileContract(endpointType, {
|
||||
endpointNames: { first: ["bar", "baz"], second: ["baz"] },
|
||||
}),
|
||||
).toThrow("Client endpoint type collision: FooBarBaz")
|
||||
|
||||
const operationKey = HttpApi.make("test")
|
||||
.add(HttpApiGroup.make("a.b").add(HttpApiEndpoint.get("get", "/first", { success: Schema.String })))
|
||||
.add(HttpApiGroup.make("a").add(HttpApiEndpoint.get("b.c", "/second", { success: Schema.String })))
|
||||
expect(() => compileContract(operationKey, { endpointNames: { get: "c", "b.c": ["b", "c"] } })).toThrow(
|
||||
"Client operation key collision: a.b.c",
|
||||
)
|
||||
|
||||
const prototype = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("session").add(HttpApiEndpoint.get("get", "/session", { success: Schema.String })),
|
||||
)
|
||||
expect(() => compileContract(prototype, { groupNames: { session: "__proto__" } })).toThrow(
|
||||
"Client group name cannot be __proto__",
|
||||
)
|
||||
})
|
||||
|
||||
test("omits custom transport endpoints", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("server.pty")
|
||||
|
|
@ -358,22 +554,6 @@ describe("HttpApiCodegen.generate", () => {
|
|||
emitPromise(compileContract(api(HttpApiEndpoint.get("read", "/file/*/tail", { success: Schema.String })))),
|
||||
).toThrow("Unsupported Promise path wildcard: /file/*/tail")
|
||||
|
||||
expect(() =>
|
||||
emitPromise(
|
||||
compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.get("binary", "/binary", {
|
||||
success: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
).toThrow("Unsupported Promise success encoding: session.binary")
|
||||
|
||||
expect(() =>
|
||||
emitPromise(compileContract(api(HttpApiEndpoint.get("read", "/file/*", { success: Schema.String })))),
|
||||
).toThrow("Unsupported Promise path wildcard: /file/*")
|
||||
|
||||
expect(() =>
|
||||
emitPromise(
|
||||
compileContract(
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ export const endpointNames = {
|
|||
"integration.attempt.status": "attemptStatus",
|
||||
"integration.attempt.complete": "attemptComplete",
|
||||
"integration.attempt.cancel": "attemptCancel",
|
||||
"session.context.entry.list": "listContextEntries",
|
||||
"session.context.entry.put": "putContextEntry",
|
||||
"session.context.entry.remove": "removeContextEntry",
|
||||
"session.instructions.entry.list": ["instructions", "entry", "list"],
|
||||
"session.instructions.entry.put": ["instructions", "entry", "put"],
|
||||
"session.instructions.entry.remove": ["instructions", "entry", "remove"],
|
||||
"session.revert.stage": "revertStage",
|
||||
"session.revert.clear": "revertClear",
|
||||
"session.revert.commit": "revertCommit",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
|||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionContextEntry } from "@opencode-ai/schema/session-context-entry"
|
||||
import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { AbsolutePath, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
|
|
@ -444,23 +444,23 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.context.entry.list", "/api/session/:sessionID/context-entry", {
|
||||
HttpApiEndpoint.get("session.instructions.entry.list", "/api/session/:sessionID/instructions/entries", {
|
||||
params: { sessionID: Session.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(SessionContextEntry.Info) }),
|
||||
success: Schema.Struct({ data: Schema.Array(InstructionEntry.Info) }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.context.entry.list",
|
||||
summary: "List context entries",
|
||||
description: "List API-managed context entries attached to the session's system context.",
|
||||
identifier: "v2.session.instructions.entry.list",
|
||||
summary: "List instruction entries",
|
||||
description: "List API-managed instruction entries attached to the session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("session.context.entry.put", "/api/session/:sessionID/context-entry/:key", {
|
||||
params: { sessionID: Session.ID, key: SessionContextEntry.Key },
|
||||
HttpApiEndpoint.put("session.instructions.entry.put", "/api/session/:sessionID/instructions/entries/:key", {
|
||||
params: { sessionID: Session.ID, key: InstructionEntry.Key },
|
||||
payload: Schema.Struct({ value: Schema.Json }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
|
|
@ -468,25 +468,26 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.context.entry.put",
|
||||
summary: "Put context entry",
|
||||
identifier: "v2.session.instructions.entry.put",
|
||||
summary: "Put instruction entry",
|
||||
description:
|
||||
"Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.",
|
||||
"Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("session.context.entry.remove", "/api/session/:sessionID/context-entry/:key", {
|
||||
params: { sessionID: Session.ID, key: SessionContextEntry.Key },
|
||||
HttpApiEndpoint.delete("session.instructions.entry.remove", "/api/session/:sessionID/instructions/entries/:key", {
|
||||
params: { sessionID: Session.ID, key: InstructionEntry.Key },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.context.entry.remove",
|
||||
summary: "Remove context entry",
|
||||
description: "Remove one context entry; the removal is announced to the model at the next turn boundary.",
|
||||
identifier: "v2.session.instructions.entry.remove",
|
||||
summary: "Remove instruction entry",
|
||||
description:
|
||||
"Remove one instruction entry; the removal is announced to the model at the next step boundary.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
20
packages/schema/src/instruction-entry.ts
Normal file
20
packages/schema/src/instruction-entry.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export * as InstructionEntry from "./instruction-entry.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
/**
|
||||
* Slash-free client-facing key for one API-managed instruction entry. The server
|
||||
* derives the namespaced Instructions key as `api/<key>`, keeping the
|
||||
* `api/*` namespace enforced by construction.
|
||||
*/
|
||||
export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/)).annotate({
|
||||
identifier: "InstructionEntry.Key",
|
||||
description: "Instruction entry key (lowercase alphanumerics plus . _ -)",
|
||||
})
|
||||
export type Key = typeof Key.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
key: Key,
|
||||
value: Schema.Json.annotate({ description: "JSON value attached to the session's instructions" }),
|
||||
}).annotate({ identifier: "InstructionEntry.Info" })
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
export * as SessionContextEntry from "./session-context-entry.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
/**
|
||||
* Slash-free client-facing key for one API-managed context entry. The server
|
||||
* derives the namespaced SystemContext key as `api/<key>`, keeping the
|
||||
* `api/*` namespace enforced by construction.
|
||||
*/
|
||||
export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/)).annotate({
|
||||
identifier: "SessionContextEntry.Key",
|
||||
description: "Context entry key (lowercase alphanumerics plus . _ -)",
|
||||
})
|
||||
export type Key = typeof Key.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
key: Key,
|
||||
value: Schema.Json.annotate({ description: "JSON value attached to the session's system context" }),
|
||||
}).annotate({ identifier: "SessionContextEntry.Info" })
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
|
|
@ -130,15 +130,15 @@ export const ExecutionSettled = Event.ephemeral({
|
|||
})
|
||||
export type ExecutionSettled = typeof ExecutionSettled.Type
|
||||
|
||||
export const ContextUpdated = Event.durable({
|
||||
type: "session.context.updated",
|
||||
export const InstructionsUpdated = Event.durable({
|
||||
type: "session.instructions.updated",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type ContextUpdated = typeof ContextUpdated.Type
|
||||
export type InstructionsUpdated = typeof InstructionsUpdated.Type
|
||||
|
||||
export const Synthetic = Event.durable({
|
||||
type: "session.synthetic",
|
||||
|
|
@ -494,7 +494,7 @@ export const Definitions = Event.inventory(
|
|||
PromptPromoted,
|
||||
PromptAdmitted,
|
||||
ExecutionSettled,
|
||||
ContextUpdated,
|
||||
InstructionsUpdated,
|
||||
Synthetic,
|
||||
Skill.Activated,
|
||||
Shell.Started,
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ describe("public event manifest", () => {
|
|||
"session.forked.1",
|
||||
"session.prompt.promoted.1",
|
||||
"session.prompt.admitted.1",
|
||||
"session.context.updated.1",
|
||||
|
||||
"session.instructions.updated.1",
|
||||
"session.synthetic.1",
|
||||
"session.skill.activated.1",
|
||||
"session.shell.started.1",
|
||||
|
|
|
|||
|
|
@ -185,11 +185,11 @@ it.live(
|
|||
resume: false,
|
||||
})
|
||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||
yield* opencode.sessions.putContextEntry({ sessionID: id, key: "deploy-target", value: "production" })
|
||||
yield* opencode.sessions.putContextEntry({ sessionID: id, key: "flags", value: { beta: true } })
|
||||
const contextEntries = yield* opencode.sessions.listContextEntries({ sessionID: id })
|
||||
yield* opencode.sessions.removeContextEntry({ sessionID: id, key: "flags" })
|
||||
const remainingContextEntries = yield* opencode.sessions.listContextEntries({ sessionID: id })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "deploy-target", value: "production" })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "flags", value: { beta: true } })
|
||||
const contextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||
yield* opencode.sessions.instructions.entry.remove({ sessionID: id, key: "flags" })
|
||||
const remainingContextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||
const wake = yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
||||
|
|
@ -219,7 +219,7 @@ it.live(
|
|||
opencode.sessions.log({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||
opencode.sessions.listContextEntries({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.instructions.entry.list({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ if (schemas) {
|
|||
visit({ ...document, components: { ...document.components, schemas: undefined } })
|
||||
for (const name of Object.keys(schemas)) {
|
||||
if (
|
||||
/^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionContextUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionReasoningStarted|SessionReasoningDelta|SessionReasoningEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1$/.test(
|
||||
/^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1$/.test(
|
||||
name,
|
||||
) &&
|
||||
!reachable.has(name)
|
||||
|
|
@ -100,7 +100,7 @@ await createClient({
|
|||
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
|
||||
const generatedTypes = await Bun.file(generatedTypesPath).text()
|
||||
if (
|
||||
/export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionContextUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionReasoningStarted|SessionReasoningDelta|SessionReasoningEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1 =/.test(
|
||||
/export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionInstructionsUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1 =/.test(
|
||||
generatedTypes,
|
||||
)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ import type {
|
|||
GlobalUpgradeResponses,
|
||||
InstanceDisposeErrors,
|
||||
InstanceDisposeResponses,
|
||||
InstructionEntryKey2,
|
||||
LocationRef2,
|
||||
LspStatusErrors,
|
||||
LspStatusResponses,
|
||||
|
|
@ -185,7 +186,6 @@ import type {
|
|||
SessionChildrenResponses,
|
||||
SessionCommandErrors,
|
||||
SessionCommandResponses,
|
||||
SessionContextEntryKey2,
|
||||
SessionCreateErrors,
|
||||
SessionCreateResponses,
|
||||
SessionDeleteErrors,
|
||||
|
|
@ -364,12 +364,6 @@ import type {
|
|||
V2SessionCommandResponses,
|
||||
V2SessionCompactErrors,
|
||||
V2SessionCompactResponses,
|
||||
V2SessionContextEntryListErrors,
|
||||
V2SessionContextEntryListResponses,
|
||||
V2SessionContextEntryPutErrors,
|
||||
V2SessionContextEntryPutResponses,
|
||||
V2SessionContextEntryRemoveErrors,
|
||||
V2SessionContextEntryRemoveResponses,
|
||||
V2SessionContextErrors,
|
||||
V2SessionContextResponses,
|
||||
V2SessionCreateErrors,
|
||||
|
|
@ -390,6 +384,12 @@ import type {
|
|||
V2SessionFormStateResponses,
|
||||
V2SessionGetErrors,
|
||||
V2SessionGetResponses,
|
||||
V2SessionInstructionsEntryListErrors,
|
||||
V2SessionInstructionsEntryListResponses,
|
||||
V2SessionInstructionsEntryPutErrors,
|
||||
V2SessionInstructionsEntryPutResponses,
|
||||
V2SessionInstructionsEntryRemoveErrors,
|
||||
V2SessionInstructionsEntryRemoveResponses,
|
||||
V2SessionInterruptErrors,
|
||||
V2SessionInterruptResponses,
|
||||
V2SessionListErrors,
|
||||
|
|
@ -5262,9 +5262,9 @@ export class Revert extends HeyApiClient {
|
|||
|
||||
export class Entry extends HeyApiClient {
|
||||
/**
|
||||
* List context entries
|
||||
* List instruction entries
|
||||
*
|
||||
* List API-managed context entries attached to the session's system context.
|
||||
* List API-managed instruction entries attached to the session.
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
|
|
@ -5274,25 +5274,25 @@ export class Entry extends HeyApiClient {
|
|||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }])
|
||||
return (options?.client ?? this.client).get<
|
||||
V2SessionContextEntryListResponses,
|
||||
V2SessionContextEntryListErrors,
|
||||
V2SessionInstructionsEntryListResponses,
|
||||
V2SessionInstructionsEntryListErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/session/{sessionID}/context-entry",
|
||||
url: "/api/session/{sessionID}/instructions/entries",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove context entry
|
||||
* Remove instruction entry
|
||||
*
|
||||
* Remove one context entry; the removal is announced to the model at the next turn boundary.
|
||||
* Remove one instruction entry; the removal is announced to the model at the next step boundary.
|
||||
*/
|
||||
public remove<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
key: SessionContextEntryKey2
|
||||
key: InstructionEntryKey2
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
|
|
@ -5308,25 +5308,25 @@ export class Entry extends HeyApiClient {
|
|||
],
|
||||
)
|
||||
return (options?.client ?? this.client).delete<
|
||||
V2SessionContextEntryRemoveResponses,
|
||||
V2SessionContextEntryRemoveErrors,
|
||||
V2SessionInstructionsEntryRemoveResponses,
|
||||
V2SessionInstructionsEntryRemoveErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/session/{sessionID}/context-entry/{key}",
|
||||
url: "/api/session/{sessionID}/instructions/entries/{key}",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Put context entry
|
||||
* Put instruction entry
|
||||
*
|
||||
* Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.
|
||||
* Attach or replace one durable instruction entry. Changes announce as updates at the next step boundary.
|
||||
*/
|
||||
public put<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
key: SessionContextEntryKey2
|
||||
key: InstructionEntryKey2
|
||||
value?: unknown
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
|
|
@ -5344,11 +5344,11 @@ export class Entry extends HeyApiClient {
|
|||
],
|
||||
)
|
||||
return (options?.client ?? this.client).put<
|
||||
V2SessionContextEntryPutResponses,
|
||||
V2SessionContextEntryPutErrors,
|
||||
V2SessionInstructionsEntryPutResponses,
|
||||
V2SessionInstructionsEntryPutErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/session/{sessionID}/context-entry/{key}",
|
||||
url: "/api/session/{sessionID}/instructions/entries/{key}",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
|
|
@ -5360,7 +5360,7 @@ export class Entry extends HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class Context extends HeyApiClient {
|
||||
export class Instructions extends HeyApiClient {
|
||||
private _entry?: Entry
|
||||
get entry(): Entry {
|
||||
return (this._entry ??= new Entry({ client: this.client }))
|
||||
|
|
@ -6479,9 +6479,9 @@ export class Session3 extends HeyApiClient {
|
|||
return (this._revert ??= new Revert({ client: this.client }))
|
||||
}
|
||||
|
||||
private _context?: Context
|
||||
get context2(): Context {
|
||||
return (this._context ??= new Context({ client: this.client }))
|
||||
private _instructions?: Instructions
|
||||
get instructions(): Instructions {
|
||||
return (this._instructions ??= new Instructions({ client: this.client }))
|
||||
}
|
||||
|
||||
private _form?: Form
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export type Event =
|
|||
| EventSessionPromptPromoted
|
||||
| EventSessionPromptAdmitted
|
||||
| EventSessionExecutionSettled
|
||||
| EventSessionContextUpdated
|
||||
| EventSessionInstructionsUpdated
|
||||
| EventSessionSynthetic
|
||||
| EventSessionSkillActivated
|
||||
| EventSessionShellStarted
|
||||
|
|
@ -929,7 +929,7 @@ export type GlobalEvent = {
|
|||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.context.updated"
|
||||
type: "session.instructions.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
text: string
|
||||
|
|
@ -1731,7 +1731,7 @@ export type GlobalEvent = {
|
|||
| SyncEventSessionForked
|
||||
| SyncEventSessionPromptPromoted
|
||||
| SyncEventSessionPromptAdmitted
|
||||
| SyncEventSessionContextUpdated
|
||||
| SyncEventSessionInstructionsUpdated
|
||||
| SyncEventSessionSynthetic
|
||||
| SyncEventSessionSkillActivated
|
||||
| SyncEventSessionShellStarted
|
||||
|
|
@ -2894,7 +2894,7 @@ export type SessionDurableEvent =
|
|||
| SessionForked
|
||||
| SessionPromptPromoted
|
||||
| SessionPromptAdmitted
|
||||
| SessionContextUpdated
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
|
|
@ -3035,7 +3035,7 @@ export type V2Event =
|
|||
| SessionPromptPromoted
|
||||
| SessionPromptAdmitted
|
||||
| SessionExecutionSettled
|
||||
| SessionContextUpdated
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
|
|
@ -3731,11 +3731,11 @@ export type SyncEventSessionPromptAdmitted = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionContextUpdated = {
|
||||
export type SyncEventSessionInstructionsUpdated = {
|
||||
type: "sync"
|
||||
id: string
|
||||
syncEvent: {
|
||||
type: "session.context.updated.1"
|
||||
type: "session.instructions.updated.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
|
|
@ -4527,10 +4527,10 @@ export type SessionMessage =
|
|||
| SessionMessageAssistant
|
||||
| SessionMessageCompaction
|
||||
|
||||
export type SessionContextEntryKey = string
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionContextEntryInfo = {
|
||||
key: SessionContextEntryKey
|
||||
export type InstructionEntryInfo = {
|
||||
key: InstructionEntryKey
|
||||
value: unknown
|
||||
}
|
||||
|
||||
|
|
@ -4671,13 +4671,13 @@ export type SessionPromptAdmitted = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SessionContextUpdated = {
|
||||
export type SessionInstructionsUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "session.context.updated"
|
||||
type: "session.instructions.updated"
|
||||
durable: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
|
|
@ -6839,9 +6839,9 @@ export type EventSessionExecutionSettled = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EventSessionContextUpdated = {
|
||||
export type EventSessionInstructionsUpdated = {
|
||||
id: string
|
||||
type: "session.context.updated"
|
||||
type: "session.instructions.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
text: string
|
||||
|
|
@ -8246,12 +8246,12 @@ export type SessionMessage2 =
|
|||
| SessionMessageCompaction2
|
||||
|
||||
/**
|
||||
* Context entry key (lowercase alphanumerics plus . _ -)
|
||||
* Instruction entry key (lowercase alphanumerics plus . _ -)
|
||||
*/
|
||||
export type SessionContextEntryKey2 = string
|
||||
export type InstructionEntryKey2 = string
|
||||
|
||||
export type SessionContextEntryInfo2 = {
|
||||
key: SessionContextEntryKey2
|
||||
export type InstructionEntryInfo2 = {
|
||||
key: InstructionEntryKey2
|
||||
value: unknown
|
||||
}
|
||||
|
||||
|
|
@ -8392,13 +8392,13 @@ export type SessionPromptAdmitted2 = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SessionContextUpdated2 = {
|
||||
export type SessionInstructionsUpdated2 = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "session.context.updated"
|
||||
type: "session.instructions.updated"
|
||||
durable: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
|
|
@ -8991,7 +8991,7 @@ export type SessionDurableEventV2 =
|
|||
| SessionForked2
|
||||
| SessionPromptPromoted2
|
||||
| SessionPromptAdmitted2
|
||||
| SessionContextUpdated2
|
||||
| SessionInstructionsUpdated2
|
||||
| SessionSynthetic2
|
||||
| SessionSkillActivated2
|
||||
| SessionShellStarted2
|
||||
|
|
@ -11207,7 +11207,7 @@ export type V2EventV2 =
|
|||
| SessionPromptPromoted2
|
||||
| SessionPromptAdmitted2
|
||||
| SessionExecutionSettled2
|
||||
| SessionContextUpdated2
|
||||
| SessionInstructionsUpdated2
|
||||
| SessionSynthetic2
|
||||
| SessionSkillActivated2
|
||||
| SessionShellStarted2
|
||||
|
|
@ -16421,16 +16421,16 @@ export type V2SessionContextResponses = {
|
|||
|
||||
export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses]
|
||||
|
||||
export type V2SessionContextEntryListData = {
|
||||
export type V2SessionInstructionsEntryListData = {
|
||||
body?: never
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/context-entry"
|
||||
url: "/api/session/{sessionID}/instructions/entries"
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryListErrors = {
|
||||
export type V2SessionInstructionsEntryListErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
|
|
@ -16445,31 +16445,32 @@ export type V2SessionContextEntryListErrors = {
|
|||
404: SessionNotFoundErrorV2
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryListError = V2SessionContextEntryListErrors[keyof V2SessionContextEntryListErrors]
|
||||
export type V2SessionInstructionsEntryListError =
|
||||
V2SessionInstructionsEntryListErrors[keyof V2SessionInstructionsEntryListErrors]
|
||||
|
||||
export type V2SessionContextEntryListResponses = {
|
||||
export type V2SessionInstructionsEntryListResponses = {
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
200: {
|
||||
data: Array<SessionContextEntryInfo2>
|
||||
data: Array<InstructionEntryInfo2>
|
||||
}
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryListResponse =
|
||||
V2SessionContextEntryListResponses[keyof V2SessionContextEntryListResponses]
|
||||
export type V2SessionInstructionsEntryListResponse =
|
||||
V2SessionInstructionsEntryListResponses[keyof V2SessionInstructionsEntryListResponses]
|
||||
|
||||
export type V2SessionContextEntryRemoveData = {
|
||||
export type V2SessionInstructionsEntryRemoveData = {
|
||||
body?: never
|
||||
path: {
|
||||
sessionID: string
|
||||
key: SessionContextEntryKey2
|
||||
key: InstructionEntryKey2
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/context-entry/{key}"
|
||||
url: "/api/session/{sessionID}/instructions/entries/{key}"
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryRemoveErrors = {
|
||||
export type V2SessionInstructionsEntryRemoveErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
|
|
@ -16484,32 +16485,32 @@ export type V2SessionContextEntryRemoveErrors = {
|
|||
404: SessionNotFoundErrorV2
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryRemoveError =
|
||||
V2SessionContextEntryRemoveErrors[keyof V2SessionContextEntryRemoveErrors]
|
||||
export type V2SessionInstructionsEntryRemoveError =
|
||||
V2SessionInstructionsEntryRemoveErrors[keyof V2SessionInstructionsEntryRemoveErrors]
|
||||
|
||||
export type V2SessionContextEntryRemoveResponses = {
|
||||
export type V2SessionInstructionsEntryRemoveResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryRemoveResponse =
|
||||
V2SessionContextEntryRemoveResponses[keyof V2SessionContextEntryRemoveResponses]
|
||||
export type V2SessionInstructionsEntryRemoveResponse =
|
||||
V2SessionInstructionsEntryRemoveResponses[keyof V2SessionInstructionsEntryRemoveResponses]
|
||||
|
||||
export type V2SessionContextEntryPutData = {
|
||||
export type V2SessionInstructionsEntryPutData = {
|
||||
body: {
|
||||
value: unknown
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
key: SessionContextEntryKey2
|
||||
key: InstructionEntryKey2
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/context-entry/{key}"
|
||||
url: "/api/session/{sessionID}/instructions/entries/{key}"
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryPutErrors = {
|
||||
export type V2SessionInstructionsEntryPutErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
|
|
@ -16524,17 +16525,18 @@ export type V2SessionContextEntryPutErrors = {
|
|||
404: SessionNotFoundErrorV2
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryPutError = V2SessionContextEntryPutErrors[keyof V2SessionContextEntryPutErrors]
|
||||
export type V2SessionInstructionsEntryPutError =
|
||||
V2SessionInstructionsEntryPutErrors[keyof V2SessionInstructionsEntryPutErrors]
|
||||
|
||||
export type V2SessionContextEntryPutResponses = {
|
||||
export type V2SessionInstructionsEntryPutResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SessionContextEntryPutResponse =
|
||||
V2SessionContextEntryPutResponses[keyof V2SessionContextEntryPutResponses]
|
||||
export type V2SessionInstructionsEntryPutResponse =
|
||||
V2SessionInstructionsEntryPutResponses[keyof V2SessionInstructionsEntryPutResponses]
|
||||
|
||||
export type V2SessionLogData = {
|
||||
body?: never
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
|
|
@ -541,25 +541,25 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.list",
|
||||
"session.instructions.entry.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
return { data: yield* contextEntries.list(ctx.params.sessionID) }
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
return { data: yield* instructions.list(ctx.params.sessionID) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.put",
|
||||
"session.instructions.entry.put",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
yield* contextEntries.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
yield* instructions.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.context.entry.remove",
|
||||
"session.instructions.entry.remove",
|
||||
Effect.fn(function* (ctx) {
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
yield* contextEntries.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
|
||||
const instructions = yield* InstructionEntry.Service
|
||||
yield* instructions.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
})
|
||||
})
|
||||
break
|
||||
case "session.context.updated":
|
||||
case "session.instructions.updated":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: messageIDFromEvent(event.id),
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||
}
|
||||
const subscriptions = [
|
||||
data.on("session.prompt.admitted", input),
|
||||
data.on("session.context.updated", message),
|
||||
data.on("session.instructions.updated", message),
|
||||
data.on("session.synthetic", (event) => {
|
||||
if (event.data.sessionID === sessionID() && event.data.description?.trim())
|
||||
appendMessage(event.id.replace(/^evt_/, "msg_"))
|
||||
|
|
|
|||
|
|
@ -1169,7 +1169,7 @@ test("renders admitted prompts immediately with queued marker and clears when pr
|
|||
}
|
||||
})
|
||||
|
||||
test("projects live context updates with their message ID", async () => {
|
||||
test("projects live instruction updates with their message ID", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
let sync!: ReturnType<typeof useData>
|
||||
|
|
@ -1199,21 +1199,21 @@ test("projects live context updates with their message ID", async () => {
|
|||
try {
|
||||
await mounted
|
||||
emitEvent(events, {
|
||||
id: "evt_context_1",
|
||||
id: "evt_instructions_1",
|
||||
created: 0,
|
||||
type: "session.context.updated",
|
||||
type: "session.instructions.updated",
|
||||
durable: durable("session-1"),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
text: "Updated context",
|
||||
text: "Updated instructions",
|
||||
},
|
||||
})
|
||||
|
||||
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
||||
expect(sync.session.message.list("session-1")?.[0]).toMatchObject({
|
||||
id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_context_1")),
|
||||
id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_instructions_1")),
|
||||
type: "system",
|
||||
text: "Updated context",
|
||||
text: "Updated instructions",
|
||||
time: { created: 0 },
|
||||
})
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue