diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index 37cafc75104..d10de0b2596 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -220,24 +220,54 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I Effect.map((value) => value.data), ) -type Endpoint4_17Request = Parameters[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[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[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[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[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[0] -type Endpoint4_18Input = { - readonly sessionID: Endpoint4_18Request["params"]["sessionID"] - readonly after?: Endpoint4_18Request["query"]["after"] +type Endpoint4_21Request = Parameters[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[0] -type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] } -const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) => +type Endpoint4_22Request = Parameters[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[0] -type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] } -const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) => +type Endpoint4_23Request = Parameters[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[0] -type Endpoint4_21Input = { - readonly sessionID: Endpoint4_21Request["params"]["sessionID"] - readonly messageID: Endpoint4_21Request["params"]["messageID"] +type Endpoint4_24Request = Parameters[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[0] diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 2e1180181e9..7f7bbf12232 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -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( + { + 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( + { + 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( { diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index df2b91f513e..2f84ab968d3 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -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"] diff --git a/packages/core/schema.json b/packages/core/schema.json index d0eeeebd5c4..e8a2502a348 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -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": [] -} +} \ No newline at end of file diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index e6ea4eaa147..55c1b212cda 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -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[] diff --git a/packages/core/src/database/migration/20260702134641_add_session_context_entry.ts b/packages/core/src/database/migration/20260702134641_add_session_context_entry.ts new file mode 100644 index 00000000000..13b37d7feff --- /dev/null +++ b/packages/core/src/database/migration/20260702134641_add_session_context_entry.ts @@ -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 diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index ed60fde6c55..d8f0f1c6652 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -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, diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index c4d177ef25c..4a3e29bd867 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -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, diff --git a/packages/core/src/session/context-entry.ts b/packages/core/src/session/context-entry.ts new file mode 100644 index 00000000000..96d3367b97b --- /dev/null +++ b/packages/core/src/session/context-entry.ts @@ -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> + readonly put: (input: { + readonly sessionID: SessionSchema.ID + readonly key: Key + readonly value: Schema.Json + }) => Effect.Effect + readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect + /** Produces one SystemContext source per stored entry, keyed `api/`. */ + readonly load: (sessionID: SessionSchema.ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/SessionContextEntry") {} + +const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2)) + +const renderBlock = (key: Key, value: Schema.Json) => + [``, renderValue(value), ""].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] }) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index e87a7573b9c..43fb00aa6ce 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -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() 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, diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 5ff659be220..d1e923b40f5 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -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 @@ -165,6 +166,20 @@ export const SessionInputTable = sqliteTable( ], ) +export const SessionContextEntryTable = sqliteTable( + "session_context_entry", + { + session_id: text() + .$type() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + key: text().notNull(), + value: text({ mode: "json" }).notNull().$type(), + ...Timestamps, + }, + (table) => [primaryKey({ columns: [table.session_id, table.key] })], +) + export const SessionContextCheckpointTable = sqliteTable("session_context_epoch", { session_id: text() .$type() diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 0b164701306..2cf947aa841 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -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":', + '', + "production", + "", + ].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:', + '', + "{", + ' "region": "us-east-1"', + "}", + "", + ].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 diff --git a/packages/plugin/src/v2/effect/generated/api.ts b/packages/plugin/src/v2/effect/generated/api.ts index be7f0815200..d85ba66cc5e 100644 --- a/packages/plugin/src/v2/effect/generated/api.ts +++ b/packages/plugin/src/v2/effect/generated/api.ts @@ -170,40 +170,70 @@ export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["param export type Endpoint4_16Output = EffectValue>["data"] export type SessionContextOperation = (input: Endpoint4_16Input) => Effect.Effect -type Endpoint4_17Request = Parameters[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> -export type SessionHistoryOperation = (input: Endpoint4_17Input) => Effect.Effect +type Endpoint4_17Request = Parameters[0] +export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] } +export type Endpoint4_17Output = EffectValue< + ReturnType +>["data"] +export type SessionListContextEntriesOperation = ( + input: Endpoint4_17Input, +) => Effect.Effect -type Endpoint4_18Request = Parameters[0] +type Endpoint4_18Request = Parameters[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>> -export type SessionEventsOperation = (input: Endpoint4_18Input) => Stream.Stream +export type Endpoint4_18Output = EffectValue> +export type SessionPutContextEntryOperation = ( + input: Endpoint4_18Input, +) => Effect.Effect -type Endpoint4_19Request = Parameters[0] -export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] } -export type Endpoint4_19Output = EffectValue> -export type SessionInterruptOperation = (input: Endpoint4_19Input) => Effect.Effect +type Endpoint4_19Request = Parameters[0] +export type Endpoint4_19Input = { + readonly sessionID: Endpoint4_19Request["params"]["sessionID"] + readonly key: Endpoint4_19Request["params"]["key"] +} +export type Endpoint4_19Output = EffectValue> +export type SessionRemoveContextEntryOperation = ( + input: Endpoint4_19Input, +) => Effect.Effect -type Endpoint4_20Request = Parameters[0] -export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] } -export type Endpoint4_20Output = EffectValue> -export type SessionBackgroundOperation = (input: Endpoint4_20Input) => Effect.Effect +type Endpoint4_20Request = Parameters[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> +export type SessionHistoryOperation = (input: Endpoint4_20Input) => Effect.Effect -type Endpoint4_21Request = Parameters[0] +type Endpoint4_21Request = Parameters[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>["data"] -export type SessionMessageOperation = (input: Endpoint4_21Input) => Effect.Effect +export type Endpoint4_21Output = StreamValue>> +export type SessionEventsOperation = (input: Endpoint4_21Input) => Stream.Stream + +type Endpoint4_22Request = Parameters[0] +export type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] } +export type Endpoint4_22Output = EffectValue> +export type SessionInterruptOperation = (input: Endpoint4_22Input) => Effect.Effect + +type Endpoint4_23Request = Parameters[0] +export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] } +export type Endpoint4_23Output = EffectValue> +export type SessionBackgroundOperation = (input: Endpoint4_23Input) => Effect.Effect + +type Endpoint4_24Request = Parameters[0] +export type Endpoint4_24Input = { + readonly sessionID: Endpoint4_24Request["params"]["sessionID"] + readonly messageID: Endpoint4_24Request["params"]["messageID"] +} +export type Endpoint4_24Output = EffectValue>["data"] +export type SessionMessageOperation = (input: Endpoint4_24Input) => Effect.Effect export interface SessionApi { readonly list: SessionListOperation @@ -223,6 +253,9 @@ export interface SessionApi { readonly revertClear: SessionRevertClearOperation readonly revertCommit: SessionRevertCommitOperation readonly context: SessionContextOperation + readonly listContextEntries: SessionListContextEntriesOperation + readonly putContextEntry: SessionPutContextEntryOperation + readonly removeContextEntry: SessionRemoveContextEntryOperation readonly history: SessionHistoryOperation readonly events: SessionEventsOperation readonly interrupt: SessionInterruptOperation diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 30392887a39..0744b15e943 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -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", diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 643bf6f4a99..96b6fd23988 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -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 = (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 }, diff --git a/packages/schema/src/session-context-entry.ts b/packages/schema/src/session-context-entry.ts new file mode 100644 index 00000000000..fbaeb530628 --- /dev/null +++ b/packages/schema/src/session-context-entry.ts @@ -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/`, 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 {} diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index 8f19ce3abc5..ae8b54d648c 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -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") }), diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index a61571e9a51..1da980481a0 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -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) {