feat(core): API-managed per-session context entries (#34941)
This commit is contained in:
parent
4045041554
commit
7843f8fb38
18 changed files with 783 additions and 146 deletions
|
|
@ -220,24 +220,54 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||||
type Endpoint4_17Input = {
|
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
|
||||||
readonly limit?: Endpoint4_17Request["query"]["limit"]
|
|
||||||
readonly after?: Endpoint4_17Request["query"]["after"]
|
|
||||||
}
|
|
||||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
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"]({
|
raw["session.history"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
query: { limit: input["limit"], after: input["after"] },
|
query: { limit: input["limit"], after: input["after"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||||
type Endpoint4_18Input = {
|
type Endpoint4_21Input = {
|
||||||
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_18Request["query"]["after"]
|
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(
|
Stream.unwrap(
|
||||||
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
|
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
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_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
|
||||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
||||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint4_21Input = {
|
type Endpoint4_24Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_21Request["params"]["messageID"]
|
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(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
|
|
@ -284,11 +314,14 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
||||||
revertClear: Endpoint4_14(raw),
|
revertClear: Endpoint4_14(raw),
|
||||||
revertCommit: Endpoint4_15(raw),
|
revertCommit: Endpoint4_15(raw),
|
||||||
context: Endpoint4_16(raw),
|
context: Endpoint4_16(raw),
|
||||||
history: Endpoint4_17(raw),
|
listContextEntries: Endpoint4_17(raw),
|
||||||
events: Endpoint4_18(raw),
|
putContextEntry: Endpoint4_18(raw),
|
||||||
interrupt: Endpoint4_19(raw),
|
removeContextEntry: Endpoint4_19(raw),
|
||||||
background: Endpoint4_20(raw),
|
history: Endpoint4_20(raw),
|
||||||
message: Endpoint4_21(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]
|
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ import type {
|
||||||
SessionRevertCommitOutput,
|
SessionRevertCommitOutput,
|
||||||
SessionContextInput,
|
SessionContextInput,
|
||||||
SessionContextOutput,
|
SessionContextOutput,
|
||||||
|
SessionListContextEntriesInput,
|
||||||
|
SessionListContextEntriesOutput,
|
||||||
|
SessionPutContextEntryInput,
|
||||||
|
SessionPutContextEntryOutput,
|
||||||
|
SessionRemoveContextEntryInput,
|
||||||
|
SessionRemoveContextEntryOutput,
|
||||||
SessionHistoryInput,
|
SessionHistoryInput,
|
||||||
SessionHistoryOutput,
|
SessionHistoryOutput,
|
||||||
SessionEventsInput,
|
SessionEventsInput,
|
||||||
|
|
@ -544,6 +550,40 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).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) =>
|
history: (input: SessionHistoryInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionHistoryOutput>(
|
request<SessionHistoryOutput>(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -809,6 +809,27 @@ export type SessionContextOutput = {
|
||||||
>
|
>
|
||||||
}["data"]
|
}["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 = {
|
export type SessionHistoryInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"]
|
readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
|
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
|
||||||
"prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
|
"prevIds": [
|
||||||
|
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
|
||||||
|
],
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
|
@ -60,6 +62,10 @@
|
||||||
"name": "session_context_epoch",
|
"name": "session_context_epoch",
|
||||||
"entityType": "tables"
|
"entityType": "tables"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "session_context_entry",
|
||||||
|
"entityType": "tables"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "session_input",
|
"name": "session_input",
|
||||||
"entityType": "tables"
|
"entityType": "tables"
|
||||||
|
|
@ -920,6 +926,56 @@
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_context_epoch"
|
"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",
|
"type": "text",
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
|
|
@ -1481,9 +1537,13 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1492,9 +1552,13 @@
|
||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["active_account_id"],
|
"columns": [
|
||||||
|
"active_account_id"
|
||||||
|
],
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1503,9 +1567,13 @@
|
||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": ["aggregate_id"],
|
"columnsTo": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1514,9 +1582,13 @@
|
||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1525,9 +1597,13 @@
|
||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1536,9 +1612,13 @@
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1547,9 +1627,13 @@
|
||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["message_id"],
|
"columns": [
|
||||||
|
"message_id"
|
||||||
|
],
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1558,9 +1642,13 @@
|
||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1569,9 +1657,28 @@
|
||||||
"table": "session_context_epoch"
|
"table": "session_context_epoch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"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",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1580,9 +1687,13 @@
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1591,9 +1702,13 @@
|
||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1602,9 +1717,13 @@
|
||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1613,9 +1732,13 @@
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1624,133 +1747,184 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["email", "url"],
|
"columns": [
|
||||||
|
"email",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id", "directory"],
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"directory"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_directory_pk",
|
"name": "project_directory_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "project_directory"
|
"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,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["name"],
|
"columns": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "credential_pk",
|
"name": "credential_pk",
|
||||||
"table": "credential",
|
"table": "credential",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_context_epoch_pk",
|
"name": "session_context_epoch_pk",
|
||||||
"table": "session_context_epoch",
|
"table": "session_context_epoch",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_pk",
|
"name": "session_input_pk",
|
||||||
"table": "session_input",
|
"table": "session_input",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
|
|
@ -2068,4 +2242,4 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"renames": []
|
"renames": []
|
||||||
}
|
}
|
||||||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -40,5 +40,6 @@ export const migrations = (
|
||||||
import("./migration/20260622142730_simplify_session_context_epoch"),
|
import("./migration/20260622142730_simplify_session_context_epoch"),
|
||||||
import("./migration/20260622170816_reset_v2_session_state"),
|
import("./migration/20260622170816_reset_v2_session_state"),
|
||||||
import("./migration/20260622202450_simplify_session_input"),
|
import("./migration/20260622202450_simplify_session_input"),
|
||||||
|
import("./migration/20260702134641_add_session_context_entry"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
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(`
|
yield* tx.run(`
|
||||||
CREATE TABLE \`session_input\` (
|
CREATE TABLE \`session_input\` (
|
||||||
\`id\` text PRIMARY KEY,
|
\`id\` text PRIMARY KEY,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import { SkillGuidance } from "./skill/guidance"
|
||||||
import { Snapshot } from "./snapshot"
|
import { Snapshot } from "./snapshot"
|
||||||
import { InstructionContext } from "./instruction-context"
|
import { InstructionContext } from "./instruction-context"
|
||||||
import { SystemContextBuiltIns } from "./system-context/builtins"
|
import { SystemContextBuiltIns } from "./system-context/builtins"
|
||||||
|
import { SessionContextEntry } from "./session/context-entry"
|
||||||
import { SessionInstructions } from "./session/instructions"
|
import { SessionInstructions } from "./session/instructions"
|
||||||
import { BuiltInTools } from "./tool/builtins"
|
import { BuiltInTools } from "./tool/builtins"
|
||||||
import { McpTool } from "./tool/mcp"
|
import { McpTool } from "./tool/mcp"
|
||||||
|
|
@ -81,6 +82,7 @@ export const locationServices = LayerNode.group([
|
||||||
SkillGuidance.node,
|
SkillGuidance.node,
|
||||||
ReferenceGuidance.node,
|
ReferenceGuidance.node,
|
||||||
SessionTodo.node,
|
SessionTodo.node,
|
||||||
|
SessionContextEntry.node,
|
||||||
QuestionV2.node,
|
QuestionV2.node,
|
||||||
Generate.node,
|
Generate.node,
|
||||||
ReadToolFileSystem.node,
|
ReadToolFileSystem.node,
|
||||||
|
|
|
||||||
108
packages/core/src/session/context-entry.ts
Normal file
108
packages/core/src/session/context-entry.ts
Normal 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] })
|
||||||
|
|
@ -23,6 +23,7 @@ import { InstructionContext } from "../../instruction-context"
|
||||||
import { SkillGuidance } from "../../skill/guidance"
|
import { SkillGuidance } from "../../skill/guidance"
|
||||||
import { ReferenceGuidance } from "../../reference/guidance"
|
import { ReferenceGuidance } from "../../reference/guidance"
|
||||||
import { McpGuidance } from "../../mcp/guidance"
|
import { McpGuidance } from "../../mcp/guidance"
|
||||||
|
import { SessionContextEntry } from "../context-entry"
|
||||||
import { ToolRegistry } from "../../tool/registry"
|
import { ToolRegistry } from "../../tool/registry"
|
||||||
import { ToolOutputStore } from "../../tool-output-store"
|
import { ToolOutputStore } from "../../tool-output-store"
|
||||||
import { SessionContextCheckpoint } from "../context-checkpoint"
|
import { SessionContextCheckpoint } from "../context-checkpoint"
|
||||||
|
|
@ -108,6 +109,7 @@ const layer = Layer.effect(
|
||||||
const skillGuidance = yield* SkillGuidance.Service
|
const skillGuidance = yield* SkillGuidance.Service
|
||||||
const referenceGuidance = yield* ReferenceGuidance.Service
|
const referenceGuidance = yield* ReferenceGuidance.Service
|
||||||
const mcpGuidance = yield* McpGuidance.Service
|
const mcpGuidance = yield* McpGuidance.Service
|
||||||
|
const contextEntries = yield* SessionContextEntry.Service
|
||||||
const snapshots = yield* Snapshot.Service
|
const snapshots = yield* Snapshot.Service
|
||||||
const db = (yield* Database.Service).db
|
const db = (yield* Database.Service).db
|
||||||
const compaction = yield* SessionCompaction.Service
|
const compaction = yield* SessionCompaction.Service
|
||||||
|
|
@ -171,7 +173,7 @@ const layer = Layer.effect(
|
||||||
const continueAfterOverflowCompaction = (step: number) =>
|
const continueAfterOverflowCompaction = (step: number) =>
|
||||||
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
|
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
|
||||||
|
|
||||||
const loadSystemContext = (agent: AgentV2.Selection) =>
|
const loadSystemContext = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
|
||||||
Effect.all(
|
Effect.all(
|
||||||
[
|
[
|
||||||
builtins.load(),
|
builtins.load(),
|
||||||
|
|
@ -179,6 +181,7 @@ const layer = Layer.effect(
|
||||||
skillGuidance.load(agent),
|
skillGuidance.load(agent),
|
||||||
referenceGuidance.load(),
|
referenceGuidance.load(),
|
||||||
mcpGuidance.load(agent),
|
mcpGuidance.load(agent),
|
||||||
|
contextEntries.load(sessionID),
|
||||||
],
|
],
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
).pipe(Effect.map(SystemContext.combine))
|
).pipe(Effect.map(SystemContext.combine))
|
||||||
|
|
@ -195,7 +198,12 @@ const layer = Layer.effect(
|
||||||
const agent = yield* agents.select(session.agent)
|
const agent = yield* agents.select(session.agent)
|
||||||
// Establish what the model knows before admitting what the user said, so
|
// Establish what the model knows before admitting what the user said, so
|
||||||
// a blocked first turn leaves pending inputs untouched.
|
// 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>()
|
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||||
let needsContinuation = false
|
let needsContinuation = false
|
||||||
let currentStep = step
|
let currentStep = step
|
||||||
|
|
@ -437,18 +445,15 @@ const layer = Layer.effect(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const failure =
|
const failure =
|
||||||
Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause) ? Cause.squash(exit.cause) : undefined
|
Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause) ? Cause.squash(exit.cause) : undefined
|
||||||
yield* events.publish(
|
yield* events.publish(SessionEvent.ExecutionSettled, {
|
||||||
SessionEvent.ExecutionSettled,
|
sessionID: input.sessionID,
|
||||||
{
|
timestamp: yield* DateTime.now,
|
||||||
sessionID: input.sessionID,
|
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
|
||||||
timestamp: yield* DateTime.now,
|
error:
|
||||||
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
|
failure !== undefined
|
||||||
error:
|
? { type: "unknown", message: failure instanceof Error ? failure.message : String(failure) }
|
||||||
failure !== undefined
|
: undefined,
|
||||||
? { type: "unknown", message: failure instanceof Error ? failure.message : String(failure) }
|
})
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchCause(() => Effect.void),
|
Effect.catchCause(() => Effect.void),
|
||||||
Effect.asVoid,
|
Effect.asVoid,
|
||||||
|
|
@ -479,6 +484,7 @@ export const node = makeLocationNode({
|
||||||
SkillGuidance.node,
|
SkillGuidance.node,
|
||||||
ReferenceGuidance.node,
|
ReferenceGuidance.node,
|
||||||
McpGuidance.node,
|
McpGuidance.node,
|
||||||
|
SessionContextEntry.node,
|
||||||
SessionCompaction.node,
|
SessionCompaction.node,
|
||||||
SessionTitle.node,
|
SessionTitle.node,
|
||||||
Config.node,
|
Config.node,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Timestamps } from "../database/schema.sql"
|
||||||
import type { SystemContext } from "../system-context/index"
|
import type { SystemContext } from "../system-context/index"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import type { Revert } from "@opencode-ai/schema/revert"
|
import type { Revert } from "@opencode-ai/schema/revert"
|
||||||
|
import type { Schema } from "effect"
|
||||||
|
|
||||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||||
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
|
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", {
|
export const SessionContextCheckpointTable = sqliteTable("session_context_epoch", {
|
||||||
session_id: text()
|
session_id: text()
|
||||||
.$type<SessionSchema.ID>()
|
.$type<SessionSchema.ID>()
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import {
|
||||||
SessionMessageTable,
|
SessionMessageTable,
|
||||||
SessionTable,
|
SessionTable,
|
||||||
} from "@opencode-ai/core/session/sql"
|
} from "@opencode-ai/core/session/sql"
|
||||||
|
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||||
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
|
||||||
|
|
@ -270,6 +271,7 @@ const it = testEffect(
|
||||||
SessionRunnerModel.node,
|
SessionRunnerModel.node,
|
||||||
SystemContextBuiltIns.node,
|
SystemContextBuiltIns.node,
|
||||||
InstructionContext.node,
|
InstructionContext.node,
|
||||||
|
SessionContextEntry.node,
|
||||||
SkillGuidance.node,
|
SkillGuidance.node,
|
||||||
ReferenceGuidance.node,
|
ReferenceGuidance.node,
|
||||||
Config.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", () =>
|
it.effect("keeps the baseline and chronological System updates after a model switch", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
|
||||||
|
|
@ -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 Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||||
export type SessionContextOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
export type SessionContextOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||||
export type Endpoint4_17Input = {
|
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
export type Endpoint4_17Output = EffectValue<
|
||||||
readonly limit?: Endpoint4_17Request["query"]["limit"]
|
ReturnType<RawClient["server.session"]["session.context.entry.list"]>
|
||||||
readonly after?: Endpoint4_17Request["query"]["after"]
|
>["data"]
|
||||||
}
|
export type SessionListContextEntriesOperation<E = never> = (
|
||||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.history"]>>
|
input: Endpoint4_17Input,
|
||||||
export type SessionHistoryOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
) => 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 = {
|
export type Endpoint4_18Input = {
|
||||||
readonly sessionID: Endpoint4_18Request["params"]["sessionID"]
|
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 Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
|
||||||
export type SessionEventsOperation<E = never> = (input: Endpoint4_18Input) => Stream.Stream<Endpoint4_18Output, E>
|
export type SessionPutContextEntryOperation<E = never> = (
|
||||||
|
input: Endpoint4_18Input,
|
||||||
|
) => Effect.Effect<Endpoint4_18Output, E>
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||||
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
export type Endpoint4_19Input = {
|
||||||
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
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]
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
||||||
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
export type Endpoint4_20Input = {
|
||||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
|
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 = {
|
export type Endpoint4_21Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
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 Endpoint4_21Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.events"]>>>
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_21Input) => Effect.Effect<Endpoint4_21Output, E>
|
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> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
|
|
@ -223,6 +253,9 @@ export interface SessionApi<E = never> {
|
||||||
readonly revertClear: SessionRevertClearOperation<E>
|
readonly revertClear: SessionRevertClearOperation<E>
|
||||||
readonly revertCommit: SessionRevertCommitOperation<E>
|
readonly revertCommit: SessionRevertCommitOperation<E>
|
||||||
readonly context: SessionContextOperation<E>
|
readonly context: SessionContextOperation<E>
|
||||||
|
readonly listContextEntries: SessionListContextEntriesOperation<E>
|
||||||
|
readonly putContextEntry: SessionPutContextEntryOperation<E>
|
||||||
|
readonly removeContextEntry: SessionRemoveContextEntryOperation<E>
|
||||||
readonly history: SessionHistoryOperation<E>
|
readonly history: SessionHistoryOperation<E>
|
||||||
readonly events: SessionEventsOperation<E>
|
readonly events: SessionEventsOperation<E>
|
||||||
readonly interrupt: SessionInterruptOperation<E>
|
readonly interrupt: SessionInterruptOperation<E>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ export const endpointNames = {
|
||||||
"integration.attempt.status": "attemptStatus",
|
"integration.attempt.status": "attemptStatus",
|
||||||
"integration.attempt.complete": "attemptComplete",
|
"integration.attempt.complete": "attemptComplete",
|
||||||
"integration.attempt.cancel": "attemptCancel",
|
"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.stage": "revertStage",
|
||||||
"session.revert.clear": "revertClear",
|
"session.revert.clear": "revertClear",
|
||||||
"session.revert.commit": "revertCommit",
|
"session.revert.commit": "revertCommit",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||||
import { Session } from "@opencode-ai/schema/session"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
|
import { SessionContextEntry } from "@opencode-ai/schema/session-context-entry"
|
||||||
import { Project } from "@opencode-ai/schema/project"
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
||||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
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(
|
.add(
|
||||||
HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
|
HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
|
|
|
||||||
20
packages/schema/src/session-context-entry.ts
Normal file
20
packages/schema/src/session-context-entry.ts
Normal 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> {}
|
||||||
|
|
@ -68,6 +68,11 @@ it.live(
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
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({
|
const wake = yield* opencode.sessions.prompt({
|
||||||
sessionID: id,
|
sessionID: id,
|
||||||
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
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.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||||
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||||
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||||
|
opencode.sessions.listContextEntries({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||||
],
|
],
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
|
|
@ -112,6 +118,11 @@ it.live(
|
||||||
expect(admitted.sessionID).toBe(id)
|
expect(admitted.sessionID).toBe(id)
|
||||||
expect(prompted.type).toBe("session.next.prompted")
|
expect(prompted.type).toBe("session.next.prompted")
|
||||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
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(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||||
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
||||||
expect(message).toEqual(modelMessage)
|
expect(message).toEqual(modelMessage)
|
||||||
|
|
@ -119,6 +130,7 @@ it.live(
|
||||||
"SessionNotFoundError",
|
"SessionNotFoundError",
|
||||||
"SessionNotFoundError",
|
"SessionNotFoundError",
|
||||||
"SessionNotFoundError",
|
"SessionNotFoundError",
|
||||||
|
"SessionNotFoundError",
|
||||||
])
|
])
|
||||||
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
|
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
|
||||||
import { DateTime, Effect, Stream } from "effect"
|
import { DateTime, Effect, Stream } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
|
|
@ -218,45 +219,49 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||||
.handle(
|
.handle(
|
||||||
"session.skill",
|
"session.skill",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
yield* session.skill({
|
yield* session
|
||||||
sessionID: ctx.params.sessionID,
|
.skill({
|
||||||
id: ctx.payload.id,
|
sessionID: ctx.params.sessionID,
|
||||||
skill: ctx.payload.skill,
|
id: ctx.payload.id,
|
||||||
resume: ctx.payload.resume,
|
skill: ctx.payload.skill,
|
||||||
}).pipe(
|
resume: ctx.payload.resume,
|
||||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
})
|
||||||
Effect.fail(
|
.pipe(
|
||||||
new SessionNotFoundError({
|
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||||
sessionID: error.sessionID,
|
Effect.fail(
|
||||||
message: `Session not found: ${error.sessionID}`,
|
new SessionNotFoundError({
|
||||||
}),
|
sessionID: error.sessionID,
|
||||||
|
message: `Session not found: ${error.sessionID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Effect.catchTag("Session.SkillNotFoundError", (error) =>
|
||||||
Effect.catchTag("Session.SkillNotFoundError", (error) =>
|
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
|
||||||
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
return HttpApiSchema.NoContent.make()
|
return HttpApiSchema.NoContent.make()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.handle(
|
.handle(
|
||||||
"session.synthetic",
|
"session.synthetic",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
yield* session.synthetic({
|
yield* session
|
||||||
sessionID: ctx.params.sessionID,
|
.synthetic({
|
||||||
text: ctx.payload.text,
|
sessionID: ctx.params.sessionID,
|
||||||
description: ctx.payload.description,
|
text: ctx.payload.text,
|
||||||
metadata: ctx.payload.metadata,
|
description: ctx.payload.description,
|
||||||
}).pipe(
|
metadata: ctx.payload.metadata,
|
||||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
})
|
||||||
Effect.fail(
|
.pipe(
|
||||||
new SessionNotFoundError({
|
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||||
sessionID: error.sessionID,
|
Effect.fail(
|
||||||
message: `Session not found: ${error.sessionID}`,
|
new SessionNotFoundError({
|
||||||
}),
|
sessionID: error.sessionID,
|
||||||
|
message: `Session not found: ${error.sessionID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
return HttpApiSchema.NoContent.make()
|
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(
|
.handle(
|
||||||
"session.history",
|
"session.history",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue