feat(core): API-managed per-session context entries (#34941)

This commit is contained in:
Kit Langton 2026-07-02 10:05:47 -04:00 committed by GitHub
parent 4045041554
commit 7843f8fb38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 781 additions and 144 deletions

View file

@ -220,24 +220,54 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I
Effect.map((value) => value.data),
)
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly limit?: Endpoint4_17Request["query"]["limit"]
readonly after?: Endpoint4_17Request["query"]["after"]
}
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
type Endpoint4_18Input = {
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
readonly key: Endpoint4_18Request["params"]["key"]
readonly value: Endpoint4_18Request["payload"]["value"]
}
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.context.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
type Endpoint4_19Input = {
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
readonly key: Endpoint4_19Request["params"]["key"]
}
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.history"]>[0]
type Endpoint4_20Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly limit?: Endpoint4_20Request["query"]["limit"]
readonly after?: Endpoint4_20Request["query"]["after"]
}
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.history"]({
params: { sessionID: input["sessionID"] },
query: { limit: input["limit"], after: input["after"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint4_18Input = {
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
readonly after?: Endpoint4_18Request["query"]["after"]
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly after?: Endpoint4_21Request["query"]["after"]
}
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
Stream.unwrap(
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
Effect.mapError(mapClientError),
@ -245,22 +275,22 @@ const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18I
),
)
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly messageID: Endpoint4_21Request["params"]["messageID"]
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly messageID: Endpoint4_24Request["params"]["messageID"]
}
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@ -284,11 +314,14 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
revertClear: Endpoint4_14(raw),
revertCommit: Endpoint4_15(raw),
context: Endpoint4_16(raw),
history: Endpoint4_17(raw),
events: Endpoint4_18(raw),
interrupt: Endpoint4_19(raw),
background: Endpoint4_20(raw),
message: Endpoint4_21(raw),
listContextEntries: Endpoint4_17(raw),
putContextEntry: Endpoint4_18(raw),
removeContextEntry: Endpoint4_19(raw),
history: Endpoint4_20(raw),
events: Endpoint4_21(raw),
interrupt: Endpoint4_22(raw),
background: Endpoint4_23(raw),
message: Endpoint4_24(raw),
})
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]

View file

@ -39,6 +39,12 @@ import type {
SessionRevertCommitOutput,
SessionContextInput,
SessionContextOutput,
SessionListContextEntriesInput,
SessionListContextEntriesOutput,
SessionPutContextEntryInput,
SessionPutContextEntryOutput,
SessionRemoveContextEntryInput,
SessionRemoveContextEntryOutput,
SessionHistoryInput,
SessionHistoryOutput,
SessionEventsInput,
@ -544,6 +550,40 @@ 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,
),
history: (input: SessionHistoryInput, requestOptions?: RequestOptions) =>
request<SessionHistoryOutput>(
{

View file

@ -809,6 +809,27 @@ export type SessionContextOutput = {
>
}["data"]
export type SessionListContextEntriesInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionListContextEntriesOutput = {
readonly data: ReadonlyArray<{ readonly key: string; readonly value: JsonValue }>
}["data"]
export type SessionPutContextEntryInput = {
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 SessionRemoveContextEntryInput = {
readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"]
readonly key: { readonly sessionID: string; readonly key: string }["key"]
}
export type SessionRemoveContextEntryOutput = void
export type SessionHistoryInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"]

View file

@ -1,8 +1,10 @@
{
"version": "7",
"dialect": "sqlite",
"id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
"prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
"prevIds": [
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
],
"ddl": [
{
"name": "workspace",
@ -60,6 +62,10 @@
"name": "session_context_epoch",
"entityType": "tables"
},
{
"name": "session_context_entry",
"entityType": "tables"
},
{
"name": "session_input",
"entityType": "tables"
@ -920,6 +926,56 @@
"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,
@ -1481,9 +1537,13 @@
"table": "session_share"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1492,9 +1552,13 @@
"table": "workspace"
},
{
"columns": ["active_account_id"],
"columns": [
"active_account_id"
],
"tableTo": "account",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
@ -1503,9 +1567,13 @@
"table": "account_state"
},
{
"columns": ["aggregate_id"],
"columns": [
"aggregate_id"
],
"tableTo": "event_sequence",
"columnsTo": ["aggregate_id"],
"columnsTo": [
"aggregate_id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1514,9 +1582,13 @@
"table": "event"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1525,9 +1597,13 @@
"table": "permission"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1536,9 +1612,13 @@
"table": "project_directory"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1547,9 +1627,13 @@
"table": "message"
},
{
"columns": ["message_id"],
"columns": [
"message_id"
],
"tableTo": "message",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1558,9 +1642,13 @@
"table": "part"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1569,9 +1657,28 @@
"table": "session_context_epoch"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"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"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1580,9 +1687,13 @@
"table": "session_input"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1591,9 +1702,13 @@
"table": "session_message"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1602,9 +1717,13 @@
"table": "session"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1613,9 +1732,13 @@
"table": "todo"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1624,133 +1747,184 @@
"table": "session_share"
},
{
"columns": ["email", "url"],
"columns": [
"email",
"url"
],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["project_id", "directory"],
"columns": [
"project_id",
"directory"
],
"nameExplicit": false,
"name": "project_directory_pk",
"entityType": "pks",
"table": "project_directory"
},
{
"columns": ["session_id", "position"],
"columns": [
"session_id",
"key"
],
"nameExplicit": false,
"name": "session_context_entry_pk",
"entityType": "pks",
"table": "session_context_entry"
},
{
"columns": [
"session_id",
"position"
],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": ["name"],
"columns": [
"name"
],
"nameExplicit": false,
"name": "data_migration_pk",
"table": "data_migration",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "account_state_pk",
"table": "account_state",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "account_pk",
"table": "account",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "credential_pk",
"table": "credential",
"entityType": "pks"
},
{
"columns": ["aggregate_id"],
"columns": [
"aggregate_id"
],
"nameExplicit": false,
"name": "event_sequence_pk",
"table": "event_sequence",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "event_pk",
"table": "event",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "session_context_epoch_pk",
"table": "session_context_epoch",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_input_pk",
"table": "session_input",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_message_pk",
"table": "session_message",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
@ -2068,4 +2242,4 @@
}
],
"renames": []
}
}

View file

@ -40,5 +40,6 @@ export const migrations = (
import("./migration/20260622142730_simplify_session_context_epoch"),
import("./migration/20260622170816_reset_v2_session_state"),
import("./migration/20260622202450_simplify_session_input"),
import("./migration/20260702134641_add_session_context_entry"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]

View file

@ -0,0 +1,21 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260702134641_add_session_context_entry",
up(tx) {
return Effect.gen(function* () {
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
);
`)
})
},
} satisfies DatabaseMigration.Migration

View file

@ -154,6 +154,17 @@ export default {
CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_context_entry\` (
\`session_id\` text NOT NULL,
\`key\` text NOT NULL,
\`value\` text NOT NULL,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL,
CONSTRAINT \`session_context_entry_pk\` PRIMARY KEY(\`session_id\`, \`key\`),
CONSTRAINT \`fk_session_context_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_input\` (
\`id\` text PRIMARY KEY,

View file

@ -38,6 +38,7 @@ 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 { SessionInstructions } from "./session/instructions"
import { BuiltInTools } from "./tool/builtins"
import { McpTool } from "./tool/mcp"
@ -81,6 +82,7 @@ export const locationServices = LayerNode.group([
SkillGuidance.node,
ReferenceGuidance.node,
SessionTodo.node,
SessionContextEntry.node,
QuestionV2.node,
Generate.node,
ReadToolFileSystem.node,

View file

@ -0,0 +1,108 @@
export * as SessionContextEntry from "./context-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 { Database } from "../database/database"
import { makeLocationNode } from "../effect/app-node"
import { SystemContext } from "../system-context/index"
import { SessionSchema } from "./schema"
import { SessionContextEntryTable } from "./sql"
export const Key = SessionContextEntry.Key
export type Key = typeof Key.Type
export const Info = SessionContextEntry.Info
export type Info = typeof Info.Type
export interface Interface {
readonly list: (sessionID: SessionSchema.ID) => Effect.Effect<ReadonlyArray<Info>>
readonly put: (input: {
readonly sessionID: SessionSchema.ID
readonly key: Key
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>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContextEntry") {}
const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2))
const renderBlock = (key: Key, value: Schema.Json) =>
[`<context key="${key}">`, renderValue(value), "</context>"].join("\n")
const source = (entry: Info) =>
SystemContext.make({
key: SystemContext.Key.make(`api/${entry.key}`),
codec: Schema.toCodecJson(Schema.Json),
load: Effect.succeed(entry.value),
baseline: (value) =>
[
`An API client attached the following context to this session under "${entry.key}":`,
renderBlock(entry.key, value),
].join("\n"),
update: (_previous, value) =>
[
`The attached context "${entry.key}" changed. This value supersedes the previous one:`,
renderBlock(entry.key, value),
].join("\n"),
removed: () => `The attached context "${entry.key}" was removed. Disregard it.`,
})
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
const list = Effect.fn("SessionContextEntry.list")(function* (sessionID: SessionSchema.ID) {
const rows = yield* db
.select()
.from(SessionContextEntryTable)
.where(eq(SessionContextEntryTable.session_id, sessionID))
.orderBy(asc(SessionContextEntryTable.key))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({ key: row.key, value: row.value }))
})
const put = Effect.fn("SessionContextEntry.put")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly key: Key
readonly value: Schema.Json
}) {
yield* db
.insert(SessionContextEntryTable)
.values({ session_id: input.sessionID, key: input.key, value: input.value })
.onConflictDoUpdate({
target: [SessionContextEntryTable.session_id, SessionContextEntryTable.key],
set: { value: input.value, time_updated: Date.now() },
})
.run()
.pipe(Effect.orDie)
})
const remove = Effect.fn("SessionContextEntry.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)),
)
.run()
.pipe(Effect.orDie)
})
const load = Effect.fn("SessionContextEntry.load")(function* (sessionID: SessionSchema.ID) {
const entries = yield* list(sessionID)
return SystemContext.combine(entries.map(source))
})
return Service.of({ list, put, remove, load })
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [Database.node] })

View file

@ -23,6 +23,7 @@ import { InstructionContext } from "../../instruction-context"
import { SkillGuidance } from "../../skill/guidance"
import { ReferenceGuidance } from "../../reference/guidance"
import { McpGuidance } from "../../mcp/guidance"
import { SessionContextEntry } from "../context-entry"
import { ToolRegistry } from "../../tool/registry"
import { ToolOutputStore } from "../../tool-output-store"
import { SessionContextCheckpoint } from "../context-checkpoint"
@ -108,6 +109,7 @@ const layer = Layer.effect(
const skillGuidance = yield* SkillGuidance.Service
const referenceGuidance = yield* ReferenceGuidance.Service
const mcpGuidance = yield* McpGuidance.Service
const contextEntries = yield* SessionContextEntry.Service
const snapshots = yield* Snapshot.Service
const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service
@ -171,7 +173,7 @@ const layer = Layer.effect(
const continueAfterOverflowCompaction = (step: number) =>
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
const loadSystemContext = (agent: AgentV2.Selection) =>
const loadSystemContext = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
Effect.all(
[
builtins.load(),
@ -179,6 +181,7 @@ const layer = Layer.effect(
skillGuidance.load(agent),
referenceGuidance.load(),
mcpGuidance.load(agent),
contextEntries.load(sessionID),
],
{ concurrency: "unbounded" },
).pipe(Effect.map(SystemContext.combine))
@ -195,7 +198,12 @@ 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 turn leaves pending inputs untouched.
const checkpoint = yield* SessionContextCheckpoint.prepare(db, events, loadSystemContext(agent), session.id)
const checkpoint = yield* SessionContextCheckpoint.prepare(
db,
events,
loadSystemContext(agent, session.id),
session.id,
)
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
let needsContinuation = false
let currentStep = step
@ -437,18 +445,15 @@ const layer = Layer.effect(
Effect.gen(function* () {
const failure =
Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause) ? Cause.squash(exit.cause) : undefined
yield* events.publish(
SessionEvent.ExecutionSettled,
{
sessionID: input.sessionID,
timestamp: yield* DateTime.now,
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
error:
failure !== undefined
? { type: "unknown", message: failure instanceof Error ? failure.message : String(failure) }
: undefined,
},
)
yield* events.publish(SessionEvent.ExecutionSettled, {
sessionID: input.sessionID,
timestamp: yield* DateTime.now,
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
error:
failure !== undefined
? { type: "unknown", message: failure instanceof Error ? failure.message : String(failure) }
: undefined,
})
}).pipe(
Effect.catchCause(() => Effect.void),
Effect.asVoid,
@ -479,6 +484,7 @@ export const node = makeLocationNode({
SkillGuidance.node,
ReferenceGuidance.node,
McpGuidance.node,
SessionContextEntry.node,
SessionCompaction.node,
SessionTitle.node,
Config.node,

View file

@ -14,6 +14,7 @@ import { Timestamps } from "../database/schema.sql"
import type { SystemContext } from "../system-context/index"
import { AgentV2 } from "../agent"
import type { Revert } from "@opencode-ai/schema/revert"
import type { Schema } from "effect"
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
@ -165,6 +166,20 @@ export const SessionInputTable = sqliteTable(
],
)
export const SessionContextEntryTable = sqliteTable(
"session_context_entry",
{
session_id: text()
.$type<SessionSchema.ID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
key: text().notNull(),
value: text({ mode: "json" }).notNull().$type<Schema.Json>(),
...Timestamps,
},
(table) => [primaryKey({ columns: [table.session_id, table.key] })],
)
export const SessionContextCheckpointTable = sqliteTable("session_context_epoch", {
session_id: text()
.$type<SessionSchema.ID>()

View file

@ -50,6 +50,7 @@ import {
SessionMessageTable,
SessionTable,
} from "@opencode-ai/core/session/sql"
import { SessionContextEntry } from "@opencode-ai/core/session/context-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"
@ -270,6 +271,7 @@ const it = testEffect(
SessionRunnerModel.node,
SystemContextBuiltIns.node,
InstructionContext.node,
SessionContextEntry.node,
SkillGuidance.node,
ReferenceGuidance.node,
Config.node,
@ -1091,6 +1093,65 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("renders API context entries through the belief lifecycle", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const contextEntries = yield* SessionContextEntry.Service
yield* contextEntries.put({ sessionID, key: "deploy-target", value: "production" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
// String values render verbatim inside the tagged block at baseline.
expect(requests[0]?.system.map((part) => part.text)).toEqual([
defaultSystem,
[
"Initial context",
"",
'An API client attached the following context to this session under "deploy-target":',
'<context key="deploy-target">',
"production",
"</context>",
].join("\n"),
])
// Non-string JSON pretty-prints; the change narrates as a System update.
yield* contextEntries.put({ sessionID, key: "deploy-target", value: { region: "us-east-1" } })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
expect(requests[1]?.messages.at(1)?.content).toEqual([
{
type: "text",
text: [
'The attached context "deploy-target" changed. This value supersedes the previous one:',
'<context key="deploy-target">',
"{",
' "region": "us-east-1"',
"}",
"</context>",
].join("\n"),
},
])
expect(yield* contextEntries.list(sessionID)).toEqual([{ key: "deploy-target", value: { region: "us-east-1" } }])
// Deleting the row announces removal through the stored removal text.
yield* contextEntries.remove({ sessionID, key: "deploy-target" })
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests[2]?.messages.map((message) => message.role)).toEqual(["user", "system", "user", "system", "user"])
expect(requests[2]?.messages.at(-2)?.content).toEqual([
{ type: "text", text: 'The attached context "deploy-target" was removed. Disregard it.' },
])
expect(yield* contextEntries.list(sessionID)).toEqual([])
}),
)
it.effect("keeps the baseline and chronological System updates after a model switch", () =>
Effect.gen(function* () {
yield* setup

View file

@ -170,40 +170,70 @@ export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["param
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.history"]>[0]
export type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly limit?: Endpoint4_17Request["query"]["limit"]
readonly after?: Endpoint4_17Request["query"]["after"]
}
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>>
export type SessionHistoryOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
export type Endpoint4_17Output = EffectValue<
ReturnType<RawClient["server.session"]["session.context.entry.list"]>
>["data"]
export type SessionListContextEntriesOperation<E = never> = (
input: Endpoint4_17Input,
) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.events"]>[0]
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
export type Endpoint4_18Input = {
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
readonly after?: Endpoint4_18Request["query"]["after"]
readonly key: Endpoint4_18Request["params"]["key"]
readonly value: Endpoint4_18Request["payload"]["value"]
}
export type Endpoint4_18Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>>
export type SessionEventsOperation<E = never> = (input: Endpoint4_18Input) => Stream.Stream<Endpoint4_18Output, E>
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
export type SessionPutContextEntryOperation<E = never> = (
input: Endpoint4_18Input,
) => Effect.Effect<Endpoint4_18Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
export type Endpoint4_19Input = {
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
readonly key: Endpoint4_19Request["params"]["key"]
}
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>>
export type SessionRemoveContextEntryOperation<E = never> = (
input: Endpoint4_19Input,
) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.history"]>[0]
export type Endpoint4_20Input = {
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
readonly limit?: Endpoint4_20Request["query"]["limit"]
readonly after?: Endpoint4_20Request["query"]["after"]
}
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>>
export type SessionHistoryOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.events"]>[0]
export type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly messageID: Endpoint4_21Request["params"]["messageID"]
readonly after?: Endpoint4_21Request["query"]["after"]
}
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_21Input) => Effect.Effect<Endpoint4_21Output, E>
export type Endpoint4_21Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>>
export type SessionEventsOperation<E = never> = (input: Endpoint4_21Input) => Stream.Stream<Endpoint4_21Output, E>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_22Input) => Effect.Effect<Endpoint4_22Output, E>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly messageID: Endpoint4_24Request["params"]["messageID"]
}
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
export interface SessionApi<E = never> {
readonly list: SessionListOperation<E>
@ -223,6 +253,9 @@ 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 history: SessionHistoryOperation<E>
readonly events: SessionEventsOperation<E>
readonly interrupt: SessionInterruptOperation<E>

View file

@ -59,6 +59,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.revert.stage": "revertStage",
"session.revert.clear": "revertClear",
"session.revert.commit": "revertCommit",

View file

@ -2,6 +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 { Project } from "@opencode-ai/schema/project"
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
import { Workspace } from "@opencode-ai/schema/workspace"
@ -378,6 +379,53 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
}),
),
)
.add(
HttpApiEndpoint.get("session.context.entry.list", "/api/session/:sessionID/context-entry", {
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(SessionContextEntry.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.",
}),
),
)
.add(
HttpApiEndpoint.put("session.context.entry.put", "/api/session/:sessionID/context-entry/:key", {
params: { sessionID: Session.ID, key: SessionContextEntry.Key },
payload: Schema.Struct({ value: Schema.Json }),
success: HttpApiSchema.NoContent,
error: SessionNotFoundError,
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.context.entry.put",
summary: "Put context 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.",
}),
),
)
.add(
HttpApiEndpoint.delete("session.context.entry.remove", "/api/session/:sessionID/context-entry/:key", {
params: { sessionID: Session.ID, key: SessionContextEntry.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.",
}),
),
)
.add(
HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
params: { sessionID: Session.ID },

View file

@ -0,0 +1,20 @@
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> {}

View file

@ -68,6 +68,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 })
const wake = yield* opencode.sessions.prompt({
sessionID: id,
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
@ -94,6 +99,7 @@ it.live(
opencode.sessions.events({ 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),
],
{ concurrency: "unbounded" },
)
@ -112,6 +118,11 @@ it.live(
expect(admitted.sessionID).toBe(id)
expect(prompted.type).toBe("session.next.prompted")
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
expect(contextEntries).toEqual([
{ key: "deploy-target", value: "production" },
{ key: "flags", value: { beta: true } },
])
expect(remainingContextEntries).toEqual([{ key: "deploy-target", value: "production" }])
expect(context.some((message) => message.type === "model-switched")).toBe(true)
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
expect(message).toEqual(modelMessage)
@ -119,6 +130,7 @@ it.live(
"SessionNotFoundError",
"SessionNotFoundError",
"SessionNotFoundError",
"SessionNotFoundError",
])
expect(missingMessage._tag).toBe("MessageNotFoundError")
}),

View file

@ -1,4 +1,5 @@
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
import { DateTime, Effect, Stream } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
@ -218,45 +219,49 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
.handle(
"session.skill",
Effect.fn(function* (ctx) {
yield* session.skill({
sessionID: ctx.params.sessionID,
id: ctx.payload.id,
skill: ctx.payload.skill,
resume: ctx.payload.resume,
}).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
yield* session
.skill({
sessionID: ctx.params.sessionID,
id: ctx.payload.id,
skill: ctx.payload.skill,
resume: ctx.payload.resume,
})
.pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
),
Effect.catchTag("Session.SkillNotFoundError", (error) =>
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
),
)
Effect.catchTag("Session.SkillNotFoundError", (error) =>
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.synthetic",
Effect.fn(function* (ctx) {
yield* session.synthetic({
sessionID: ctx.params.sessionID,
text: ctx.payload.text,
description: ctx.payload.description,
metadata: ctx.payload.metadata,
}).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
yield* session
.synthetic({
sessionID: ctx.params.sessionID,
text: ctx.payload.text,
description: ctx.payload.description,
metadata: ctx.payload.metadata,
})
.pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
),
)
)
return HttpApiSchema.NoContent.make()
}),
)
@ -462,6 +467,29 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}
}),
)
.handle(
"session.context.entry.list",
Effect.fn(function* (ctx) {
const contextEntries = yield* SessionContextEntry.Service
return { data: yield* contextEntries.list(ctx.params.sessionID) }
}),
)
.handle(
"session.context.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 })
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.context.entry.remove",
Effect.fn(function* (ctx) {
const contextEntries = yield* SessionContextEntry.Service
yield* contextEntries.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.history",
Effect.fn(function* (ctx) {