@opengeni/db 0.23.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-3UCHDMKG.js → chunk-EX7CDSGH.js} +196 -2
- package/dist/chunk-EX7CDSGH.js.map +1 -0
- package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
- package/dist/chunk-IHPCI4GV.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +1457 -338
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +262 -4
- package/dist/schema.js +5 -1
- package/dist/session-realtime-mirror.d.ts +22 -1
- package/dist/workspace-instruction-policies-schema.d.ts +449 -0
- package/dist/workspace-instruction-policies.d.ts +88 -8
- package/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -0
- package/package.json +3 -3
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +419 -23
- package/src/preference-registry.ts +103 -0
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +86 -0
- package/src/session-control.ts +48 -1
- package/src/session-queue-commands.ts +45 -11
- package/src/session-realtime-mirror.ts +117 -1
- package/src/session-realtime.ts +49 -1
- package/src/workspace-instruction-policies-schema.ts +116 -0
- package/src/workspace-instruction-policies.ts +819 -25
- package/dist/chunk-3UCHDMKG.js.map +0 -1
- package/dist/chunk-L6ADMZHE.js.map +0 -1
package/src/schema.ts
CHANGED
|
@@ -720,6 +720,45 @@ export const connections = pgTable(
|
|
|
720
720
|
}),
|
|
721
721
|
);
|
|
722
722
|
|
|
723
|
+
export const connectionDisconnectOperations = pgTable(
|
|
724
|
+
"connection_disconnect_operations",
|
|
725
|
+
{
|
|
726
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
727
|
+
accountId: uuid("account_id")
|
|
728
|
+
.notNull()
|
|
729
|
+
.references(() => managedAccounts.id, { onDelete: "cascade" }),
|
|
730
|
+
workspaceId: uuid("workspace_id")
|
|
731
|
+
.notNull()
|
|
732
|
+
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
733
|
+
connectionId: uuid("connection_id")
|
|
734
|
+
.notNull()
|
|
735
|
+
.references(() => connections.id, { onDelete: "cascade" }),
|
|
736
|
+
subjectId: text("subject_id").notNull(),
|
|
737
|
+
idempotencyKey: text("idempotency_key").notNull(),
|
|
738
|
+
expectedVersion: integer("expected_version").notNull(),
|
|
739
|
+
resultVersion: integer("result_version").notNull(),
|
|
740
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
741
|
+
},
|
|
742
|
+
(table) => ({
|
|
743
|
+
workspaceSubjectKey: uniqueIndex("connection_disconnect_operations_subject_key_uq").on(
|
|
744
|
+
table.workspaceId,
|
|
745
|
+
table.subjectId,
|
|
746
|
+
table.idempotencyKey,
|
|
747
|
+
),
|
|
748
|
+
connectionGeneration: uniqueIndex(
|
|
749
|
+
"connection_disconnect_operations_connection_generation_uq",
|
|
750
|
+
).on(table.workspaceId, table.connectionId, table.expectedVersion),
|
|
751
|
+
identityValid: check(
|
|
752
|
+
"connection_disconnect_operations_identity_check",
|
|
753
|
+
sql`length(${table.subjectId}) between 1 and 512
|
|
754
|
+
and length(${table.idempotencyKey}) between 1 and 200
|
|
755
|
+
and ${table.idempotencyKey} = btrim(${table.idempotencyKey})
|
|
756
|
+
and ${table.expectedVersion} > 0
|
|
757
|
+
and ${table.resultVersion} = ${table.expectedVersion} + 1`,
|
|
758
|
+
),
|
|
759
|
+
}),
|
|
760
|
+
);
|
|
761
|
+
|
|
723
762
|
export const connectorActionPolicies = pgTable(
|
|
724
763
|
"connector_action_policies",
|
|
725
764
|
{
|
|
@@ -2150,6 +2189,11 @@ export const documents = pgTable(
|
|
|
2150
2189
|
sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
|
|
2151
2190
|
sourceVersion: text("source_version"),
|
|
2152
2191
|
aclTags: jsonb("acl_tags").$type<string[]>().notNull().default([]),
|
|
2192
|
+
// Durable authorization tuple. The workspace_id above remains ingestion
|
|
2193
|
+
// provenance; organization authority deliberately has no workspace owner.
|
|
2194
|
+
authorityKind: text("authority_kind").notNull().default("workspace"),
|
|
2195
|
+
authorityWorkspaceId: uuid("authority_workspace_id"),
|
|
2196
|
+
authoritySubjectId: text("authority_subject_id"),
|
|
2153
2197
|
// Per-document access controls. visibility 'private' restricts human reads to
|
|
2154
2198
|
// created_by (a grant subject id, not a uuid); agent_access=false hides the
|
|
2155
2199
|
// document from agent retrieval surfaces (docs MCP) while humans keep REST.
|
|
@@ -2187,6 +2231,28 @@ export const documents = pgTable(
|
|
|
2187
2231
|
table.workspaceId,
|
|
2188
2232
|
table.curationStatus,
|
|
2189
2233
|
),
|
|
2234
|
+
authority: index("documents_authority_idx").on(
|
|
2235
|
+
table.accountId,
|
|
2236
|
+
table.authorityKind,
|
|
2237
|
+
table.authorityWorkspaceId,
|
|
2238
|
+
table.authoritySubjectId,
|
|
2239
|
+
table.status,
|
|
2240
|
+
),
|
|
2241
|
+
authorityWorkspaceAccount: foreignKey({
|
|
2242
|
+
name: "documents_authority_workspace_fk",
|
|
2243
|
+
columns: [table.authorityWorkspaceId, table.accountId],
|
|
2244
|
+
foreignColumns: [workspaces.id, workspaces.accountId],
|
|
2245
|
+
}).onDelete("restrict"),
|
|
2246
|
+
authorityState: check(
|
|
2247
|
+
"documents_authority_chk",
|
|
2248
|
+
sql`(${table.authorityKind} = 'organization' and ${table.authorityWorkspaceId} is null and ${table.authoritySubjectId} is null)
|
|
2249
|
+
or (${table.authorityKind} = 'workspace' and ${table.authorityWorkspaceId} = ${table.workspaceId} and ${table.authoritySubjectId} is null)
|
|
2250
|
+
or (${table.authorityKind} = 'personal' and ${table.authorityWorkspaceId} = ${table.workspaceId} and nullif(btrim(${table.authoritySubjectId}), '') is not null and octet_length(convert_to(${table.authoritySubjectId}, 'UTF8')) <= 1024 and ${table.authoritySubjectId} = ${table.createdBy})`,
|
|
2251
|
+
),
|
|
2252
|
+
authorityVisibility: check(
|
|
2253
|
+
"documents_authority_visibility_chk",
|
|
2254
|
+
sql`(${table.authorityKind} = 'personal') = (${table.visibility} = 'private')`,
|
|
2255
|
+
),
|
|
2190
2256
|
visibilityState: check(
|
|
2191
2257
|
"documents_visibility_chk",
|
|
2192
2258
|
sql`${table.visibility} in ('workspace', 'private')`,
|
|
@@ -2229,6 +2295,9 @@ export const documentChunks = pgTable(
|
|
|
2229
2295
|
chunkIndex: integer("chunk_index").notNull(),
|
|
2230
2296
|
text: text("text").notNull(),
|
|
2231
2297
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
2298
|
+
authorityKind: text("authority_kind").notNull().default("workspace"),
|
|
2299
|
+
authorityWorkspaceId: uuid("authority_workspace_id"),
|
|
2300
|
+
authoritySubjectId: text("authority_subject_id"),
|
|
2232
2301
|
embedding: vector("embedding").notNull(),
|
|
2233
2302
|
embeddingModel: text("embedding_model").notNull(),
|
|
2234
2303
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
@@ -2240,6 +2309,23 @@ export const documentChunks = pgTable(
|
|
|
2240
2309
|
table.chunkIndex,
|
|
2241
2310
|
),
|
|
2242
2311
|
base: index("document_chunks_workspace_base_idx").on(table.workspaceId, table.baseId),
|
|
2312
|
+
authority: index("document_chunks_authority_idx").on(
|
|
2313
|
+
table.accountId,
|
|
2314
|
+
table.authorityKind,
|
|
2315
|
+
table.authorityWorkspaceId,
|
|
2316
|
+
table.authoritySubjectId,
|
|
2317
|
+
),
|
|
2318
|
+
authorityWorkspaceAccount: foreignKey({
|
|
2319
|
+
name: "document_chunks_authority_workspace_fk",
|
|
2320
|
+
columns: [table.authorityWorkspaceId, table.accountId],
|
|
2321
|
+
foreignColumns: [workspaces.id, workspaces.accountId],
|
|
2322
|
+
}).onDelete("restrict"),
|
|
2323
|
+
authorityState: check(
|
|
2324
|
+
"document_chunks_authority_chk",
|
|
2325
|
+
sql`(${table.authorityKind} = 'organization' and ${table.authorityWorkspaceId} is null and ${table.authoritySubjectId} is null)
|
|
2326
|
+
or (${table.authorityKind} = 'workspace' and ${table.authorityWorkspaceId} = ${table.workspaceId} and ${table.authoritySubjectId} is null)
|
|
2327
|
+
or (${table.authorityKind} = 'personal' and ${table.authorityWorkspaceId} = ${table.workspaceId} and nullif(btrim(${table.authoritySubjectId}), '') is not null and octet_length(convert_to(${table.authoritySubjectId}, 'UTF8')) <= 1024)`,
|
|
2328
|
+
),
|
|
2243
2329
|
}),
|
|
2244
2330
|
);
|
|
2245
2331
|
|
package/src/session-control.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { and, eq, inArray, sql } from "drizzle-orm";
|
|
|
10
10
|
import type { Database } from "./index";
|
|
11
11
|
import * as schema from "./schema";
|
|
12
12
|
import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
|
|
13
|
+
import {
|
|
14
|
+
mirrorSessionRealtimeContextInTransaction,
|
|
15
|
+
renderRealtimeHumanInputResponseContext,
|
|
16
|
+
} from "./session-realtime-mirror";
|
|
13
17
|
|
|
14
18
|
export const SESSION_ANCESTRY_LIMIT = 10_000;
|
|
15
19
|
|
|
@@ -2088,7 +2092,12 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2088
2092
|
}
|
|
2089
2093
|
const immediatelyCancelledTurnIds = immediatelyCancelledTurns.map((turn) => turn.id);
|
|
2090
2094
|
const now = new Date();
|
|
2091
|
-
let cancelledHumanInputs: Array<{
|
|
2095
|
+
let cancelledHumanInputs: Array<{
|
|
2096
|
+
id: string;
|
|
2097
|
+
sessionId: string;
|
|
2098
|
+
turnId: string;
|
|
2099
|
+
questions: (typeof schema.sessionHumanInputRequests.$inferSelect)["questions"];
|
|
2100
|
+
}> = [];
|
|
2092
2101
|
if (immediatelyCancelledTurnIds.length > 0) {
|
|
2093
2102
|
await db
|
|
2094
2103
|
.update(schema.sessionTurns)
|
|
@@ -2122,6 +2131,7 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2122
2131
|
id: schema.sessionHumanInputRequests.id,
|
|
2123
2132
|
sessionId: schema.sessionHumanInputRequests.sessionId,
|
|
2124
2133
|
turnId: schema.sessionHumanInputRequests.turnId,
|
|
2134
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
2125
2135
|
});
|
|
2126
2136
|
await db
|
|
2127
2137
|
.update(schema.codexCapacityWaiters)
|
|
@@ -2247,8 +2257,45 @@ async function cancelSessionSubtreeInTransaction(
|
|
|
2247
2257
|
? await db.insert(schema.sessionEvents).values(eventValues).returning({
|
|
2248
2258
|
id: schema.sessionEvents.id,
|
|
2249
2259
|
sequence: schema.sessionEvents.sequence,
|
|
2260
|
+
type: schema.sessionEvents.type,
|
|
2261
|
+
turnId: schema.sessionEvents.turnId,
|
|
2262
|
+
payload: schema.sessionEvents.payload,
|
|
2250
2263
|
})
|
|
2251
2264
|
: [];
|
|
2265
|
+
const cancelledHumanInputsById = new Map(
|
|
2266
|
+
cancelledHumanInputs
|
|
2267
|
+
.filter((request) => request.sessionId === session.id)
|
|
2268
|
+
.map((request) => [request.id, request]),
|
|
2269
|
+
);
|
|
2270
|
+
for (const event of inserted) {
|
|
2271
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
2272
|
+
const payload = event.payload as { requestId?: unknown };
|
|
2273
|
+
const request =
|
|
2274
|
+
typeof payload.requestId === "string"
|
|
2275
|
+
? cancelledHumanInputsById.get(payload.requestId)
|
|
2276
|
+
: null;
|
|
2277
|
+
if (!request) continue;
|
|
2278
|
+
await mirrorSessionRealtimeContextInTransaction(db, {
|
|
2279
|
+
accountId: input.accountId,
|
|
2280
|
+
workspaceId: input.workspaceId,
|
|
2281
|
+
sessionId: session.id,
|
|
2282
|
+
sourceKind: "human_input_response",
|
|
2283
|
+
sourceId: event.id,
|
|
2284
|
+
turnId: event.turnId,
|
|
2285
|
+
channel: null,
|
|
2286
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
2287
|
+
requestId: request.id,
|
|
2288
|
+
questions: request.questions,
|
|
2289
|
+
response: { outcome: "cancelled" },
|
|
2290
|
+
}),
|
|
2291
|
+
payload: {
|
|
2292
|
+
requestId: request.id,
|
|
2293
|
+
outcome: "cancelled",
|
|
2294
|
+
sourceEventId: event.id,
|
|
2295
|
+
},
|
|
2296
|
+
now,
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2252
2299
|
const liveTurnId =
|
|
2253
2300
|
session.activeTurnId && liveTurnIds.has(session.activeTurnId) ? session.activeTurnId : null;
|
|
2254
2301
|
await db
|
|
@@ -35,6 +35,7 @@ import { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
|
|
|
35
35
|
import {
|
|
36
36
|
mirrorSessionRealtimeContextInTransaction,
|
|
37
37
|
renderRealtimeHumanInputContext,
|
|
38
|
+
renderRealtimeHumanInputResponseContext,
|
|
38
39
|
} from "./session-realtime-mirror";
|
|
39
40
|
import * as schema from "./schema";
|
|
40
41
|
import {
|
|
@@ -284,23 +285,56 @@ export async function supersedeSessionCurrentDirectionInTransaction(
|
|
|
284
285
|
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
285
286
|
),
|
|
286
287
|
)
|
|
287
|
-
.returning({
|
|
288
|
+
.returning({
|
|
289
|
+
id: schema.sessionHumanInputRequests.id,
|
|
290
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
291
|
+
});
|
|
288
292
|
let lastSequence = closedTools.sequence;
|
|
289
293
|
if (cancelledHumanInputs.length > 0) {
|
|
290
|
-
await db
|
|
291
|
-
|
|
294
|
+
const cancelledHumanInputEvents = await db
|
|
295
|
+
.insert(schema.sessionEvents)
|
|
296
|
+
.values(
|
|
297
|
+
cancelledHumanInputs.map((request) => ({
|
|
298
|
+
accountId: input.accountId,
|
|
299
|
+
workspaceId: input.workspaceId,
|
|
300
|
+
sessionId: input.sessionId,
|
|
301
|
+
sequence: ++lastSequence,
|
|
302
|
+
type: "user.humanInputResponse",
|
|
303
|
+
turnId: current.id,
|
|
304
|
+
turnGeneration: current.executionGeneration,
|
|
305
|
+
turnAssociation: "current",
|
|
306
|
+
payload: { requestId: request.id, response: { outcome: "cancelled" } },
|
|
307
|
+
occurredAt: now,
|
|
308
|
+
})),
|
|
309
|
+
)
|
|
310
|
+
.returning();
|
|
311
|
+
const requestsById = new Map(cancelledHumanInputs.map((request) => [request.id, request]));
|
|
312
|
+
for (const event of cancelledHumanInputEvents) {
|
|
313
|
+
const payload = event.payload as { requestId?: unknown };
|
|
314
|
+
const request =
|
|
315
|
+
typeof payload.requestId === "string" ? requestsById.get(payload.requestId) : null;
|
|
316
|
+
if (!request) continue;
|
|
317
|
+
await mirrorSessionRealtimeContextInTransaction(db, {
|
|
292
318
|
accountId: input.accountId,
|
|
293
319
|
workspaceId: input.workspaceId,
|
|
294
320
|
sessionId: input.sessionId,
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
sourceKind: "human_input_response",
|
|
322
|
+
sourceId: event.id,
|
|
297
323
|
turnId: current.id,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
channel: null,
|
|
325
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
326
|
+
requestId: request.id,
|
|
327
|
+
questions: request.questions,
|
|
328
|
+
response: { outcome: "cancelled" },
|
|
329
|
+
}),
|
|
330
|
+
payload: {
|
|
331
|
+
requestId: request.id,
|
|
332
|
+
outcome: "cancelled",
|
|
333
|
+
sourceEventId: event.id,
|
|
334
|
+
},
|
|
335
|
+
now,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
304
338
|
}
|
|
305
339
|
await db
|
|
306
340
|
.update(schema.sessionTurns)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import type { HumanInputQuestion, HumanInputResponse } from "@opengeni/contracts";
|
|
3
4
|
import { and, desc, eq, gt, sql } from "drizzle-orm";
|
|
4
5
|
|
|
5
6
|
import { sanitizeEventPayload } from "./event-payload-sanitizer";
|
|
@@ -16,7 +17,12 @@ export type MirrorSessionRealtimeContextInput = {
|
|
|
16
17
|
accountId: string;
|
|
17
18
|
workspaceId: string;
|
|
18
19
|
sessionId: string;
|
|
19
|
-
sourceKind:
|
|
20
|
+
sourceKind:
|
|
21
|
+
| "human_input"
|
|
22
|
+
| "human_input_request"
|
|
23
|
+
| "human_input_response"
|
|
24
|
+
| "assistant_progress"
|
|
25
|
+
| "assistant_terminal";
|
|
20
26
|
sourceId: string;
|
|
21
27
|
text: string;
|
|
22
28
|
channel: SessionRealtimeMirrorChannel;
|
|
@@ -155,6 +161,116 @@ export function renderRealtimeHumanInputContext(input: {
|
|
|
155
161
|
].join("\n");
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
export type RealtimeHumanInputRequestContext = {
|
|
165
|
+
id: string;
|
|
166
|
+
questions: HumanInputQuestion[];
|
|
167
|
+
allowSkip: boolean;
|
|
168
|
+
expiresAt?: Date | string | null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Render the exact durable question contract for the conversational surface.
|
|
173
|
+
* The structured form remains authoritative, while a conversational answer is
|
|
174
|
+
* ordinary new user input that the realtime model delegates once with enough
|
|
175
|
+
* question context for the next underlying turn.
|
|
176
|
+
*/
|
|
177
|
+
export function renderRealtimeHumanInputRequestContext(input: {
|
|
178
|
+
requests: RealtimeHumanInputRequestContext[];
|
|
179
|
+
}): string {
|
|
180
|
+
const lines = ["<session_human_input_request>", " <status>waiting_for_user</status>"];
|
|
181
|
+
for (const request of input.requests) {
|
|
182
|
+
lines.push(
|
|
183
|
+
" <request>",
|
|
184
|
+
` <id>${escapeXmlText(request.id)}</id>`,
|
|
185
|
+
` <allow_skip>${String(request.allowSkip)}</allow_skip>`,
|
|
186
|
+
` <expires_at>${escapeXmlText(renderExpiry(request.expiresAt))}</expires_at>`,
|
|
187
|
+
);
|
|
188
|
+
for (const question of request.questions) {
|
|
189
|
+
lines.push(
|
|
190
|
+
" <question>",
|
|
191
|
+
` <id>${escapeXmlText(question.id)}</id>`,
|
|
192
|
+
` <kind>${escapeXmlText(question.kind)}</kind>`,
|
|
193
|
+
` <required>${String(question.required)}</required>`,
|
|
194
|
+
);
|
|
195
|
+
if (question.label) lines.push(` <label>${escapeXmlText(question.label)}</label>`);
|
|
196
|
+
lines.push(` <prompt>${escapeXmlText(question.prompt)}</prompt>`);
|
|
197
|
+
if (question.helpText) {
|
|
198
|
+
lines.push(` <help_text>${escapeXmlText(question.helpText)}</help_text>`);
|
|
199
|
+
}
|
|
200
|
+
if (question.options.length > 0) {
|
|
201
|
+
lines.push(" <options>");
|
|
202
|
+
for (const option of question.options) {
|
|
203
|
+
lines.push(
|
|
204
|
+
" <option>",
|
|
205
|
+
` <id>${escapeXmlText(option.id)}</id>`,
|
|
206
|
+
` <label>${escapeXmlText(option.label)}</label>`,
|
|
207
|
+
);
|
|
208
|
+
if (option.description) {
|
|
209
|
+
lines.push(` <description>${escapeXmlText(option.description)}</description>`);
|
|
210
|
+
}
|
|
211
|
+
lines.push(" </option>");
|
|
212
|
+
}
|
|
213
|
+
lines.push(" </options>");
|
|
214
|
+
}
|
|
215
|
+
lines.push(
|
|
216
|
+
` <allow_other>${String(question.allowOther)}</allow_other>`,
|
|
217
|
+
" </question>",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
lines.push(" </request>");
|
|
221
|
+
}
|
|
222
|
+
lines.push(
|
|
223
|
+
" <instruction>The current work is waiting for the user's input. Ask the questions naturally, one at a time when useful, without changing their meaning or answering for the user. The user may answer in the visible form or answer conversationally here. If the user answers here, delegate exactly once with a complete message containing the relevant question and the user's answer. If the user changes direction instead, delegate that new direction normally. Do not claim the work resumed until a later session update confirms it.</instruction>",
|
|
224
|
+
"</session_human_input_request>",
|
|
225
|
+
);
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function renderRealtimeHumanInputResponseContext(input: {
|
|
230
|
+
requestId: string;
|
|
231
|
+
questions: HumanInputQuestion[];
|
|
232
|
+
response: HumanInputResponse;
|
|
233
|
+
}): string {
|
|
234
|
+
const lines = [
|
|
235
|
+
"<session_human_input_response>",
|
|
236
|
+
` <request_id>${escapeXmlText(input.requestId)}</request_id>`,
|
|
237
|
+
` <outcome>${escapeXmlText(input.response.outcome)}</outcome>`,
|
|
238
|
+
];
|
|
239
|
+
if (input.response.outcome === "answered") {
|
|
240
|
+
const questions = new Map(input.questions.map((question) => [question.id, question]));
|
|
241
|
+
lines.push(" <answers>");
|
|
242
|
+
for (const answer of input.response.answers) {
|
|
243
|
+
const question = questions.get(answer.questionId);
|
|
244
|
+
const optionLabels = new Map(
|
|
245
|
+
question?.options.map((option) => [option.id, option.label]) ?? [],
|
|
246
|
+
);
|
|
247
|
+
lines.push(
|
|
248
|
+
" <answer>",
|
|
249
|
+
` <question_id>${escapeXmlText(answer.questionId)}</question_id>`,
|
|
250
|
+
);
|
|
251
|
+
if (question) lines.push(` <question>${escapeXmlText(question.prompt)}</question>`);
|
|
252
|
+
for (const value of answer.values) {
|
|
253
|
+
lines.push(` <value>${escapeXmlText(optionLabels.get(value) ?? value)}</value>`);
|
|
254
|
+
}
|
|
255
|
+
if (answer.other) lines.push(` <other>${escapeXmlText(answer.other)}</other>`);
|
|
256
|
+
lines.push(" </answer>");
|
|
257
|
+
}
|
|
258
|
+
lines.push(" </answers>");
|
|
259
|
+
}
|
|
260
|
+
lines.push(
|
|
261
|
+
input.response.outcome === "answered" || input.response.outcome === "skipped"
|
|
262
|
+
? " <instruction>This structured response was accepted through the session UI and the same work can resume. Treat it as authoritative user context, do not delegate it again, and acknowledge briefly only if useful.</instruction>"
|
|
263
|
+
: " <instruction>This pending question is no longer active. Do not ask it again unless the user raises it.</instruction>",
|
|
264
|
+
"</session_human_input_response>",
|
|
265
|
+
);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderExpiry(value: Date | string | null | undefined): string {
|
|
270
|
+
if (value === null || value === undefined) return "none";
|
|
271
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
272
|
+
}
|
|
273
|
+
|
|
158
274
|
function escapeXmlText(value: string): string {
|
|
159
275
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
160
276
|
}
|
package/src/session-realtime.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
SessionRealtimeMode,
|
|
7
7
|
SessionRealtimeModel,
|
|
8
8
|
} from "@opengeni/contracts";
|
|
9
|
-
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
9
|
+
import { and, asc, eq, gt, inArray, isNull, or } from "drizzle-orm";
|
|
10
10
|
|
|
11
11
|
import { sanitizeEventPayload } from "./event-payload-sanitizer";
|
|
12
12
|
import type { Database } from "./index";
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
lockSessionEventWriteRows,
|
|
17
17
|
registerSessionWorkflowWakeInTransaction,
|
|
18
18
|
} from "./session-control";
|
|
19
|
+
import {
|
|
20
|
+
mirrorSessionRealtimeContextInTransaction,
|
|
21
|
+
renderRealtimeHumanInputRequestContext,
|
|
22
|
+
} from "./session-realtime-mirror";
|
|
19
23
|
import * as schema from "./schema";
|
|
20
24
|
|
|
21
25
|
export { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
|
|
@@ -445,6 +449,50 @@ export async function beginSessionRealtimeInTransaction(
|
|
|
445
449
|
now,
|
|
446
450
|
);
|
|
447
451
|
eventIds.push(event.id);
|
|
452
|
+
const pendingHumanInputs = await db
|
|
453
|
+
.select({
|
|
454
|
+
id: schema.sessionHumanInputRequests.id,
|
|
455
|
+
turnId: schema.sessionHumanInputRequests.turnId,
|
|
456
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
457
|
+
allowSkip: schema.sessionHumanInputRequests.allowSkip,
|
|
458
|
+
expiresAt: schema.sessionHumanInputRequests.expiresAt,
|
|
459
|
+
})
|
|
460
|
+
.from(schema.sessionHumanInputRequests)
|
|
461
|
+
.where(
|
|
462
|
+
and(
|
|
463
|
+
eq(schema.sessionHumanInputRequests.accountId, input.accountId),
|
|
464
|
+
eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
|
|
465
|
+
eq(schema.sessionHumanInputRequests.sessionId, input.sessionId),
|
|
466
|
+
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
467
|
+
or(
|
|
468
|
+
isNull(schema.sessionHumanInputRequests.expiresAt),
|
|
469
|
+
gt(schema.sessionHumanInputRequests.expiresAt, now),
|
|
470
|
+
),
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
.orderBy(
|
|
474
|
+
asc(schema.sessionHumanInputRequests.createdAt),
|
|
475
|
+
asc(schema.sessionHumanInputRequests.id),
|
|
476
|
+
);
|
|
477
|
+
if (pendingHumanInputs.length > 0) {
|
|
478
|
+
const sourceTurnIds = new Set(pendingHumanInputs.map((request) => request.turnId));
|
|
479
|
+
await mirrorSessionRealtimeContextInTransaction(db, {
|
|
480
|
+
accountId: input.accountId,
|
|
481
|
+
workspaceId: input.workspaceId,
|
|
482
|
+
sessionId: input.sessionId,
|
|
483
|
+
sourceKind: "human_input_request",
|
|
484
|
+
sourceId: `startup:${pendingHumanInputs.map((request) => request.id).join(":")}`,
|
|
485
|
+
turnId: sourceTurnIds.size === 1 ? pendingHumanInputs[0]!.turnId : null,
|
|
486
|
+
channel: "speakable",
|
|
487
|
+
text: renderRealtimeHumanInputRequestContext({ requests: pendingHumanInputs }),
|
|
488
|
+
payload: {
|
|
489
|
+
status: "waiting_for_user",
|
|
490
|
+
requestIds: pendingHumanInputs.map((request) => request.id),
|
|
491
|
+
trigger: "realtime_start",
|
|
492
|
+
},
|
|
493
|
+
now,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
448
496
|
return {
|
|
449
497
|
mode: mapRealtimeMode(row),
|
|
450
498
|
replay: false,
|
|
@@ -21,6 +21,8 @@ export const workspaceInstructionPolicyRevisions = pgTable(
|
|
|
21
21
|
"workspace_instruction_policy_revisions",
|
|
22
22
|
{
|
|
23
23
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
24
|
+
operationId: uuid("operation_id"),
|
|
25
|
+
requestFingerprint: text("request_fingerprint"),
|
|
24
26
|
accountId: uuid("account_id").notNull(),
|
|
25
27
|
workspaceId: uuid("workspace_id").notNull(),
|
|
26
28
|
revision: bigint("revision", { mode: "number" })
|
|
@@ -38,6 +40,19 @@ export const workspaceInstructionPolicyRevisions = pgTable(
|
|
|
38
40
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
39
41
|
},
|
|
40
42
|
(table) => ({
|
|
43
|
+
workspaceOperation: uniqueIndex("workspace_instruction_policy_revisions_workspace_operation_uq")
|
|
44
|
+
.on(table.workspaceId, table.operationId)
|
|
45
|
+
.where(sql`${table.operationId} is not null`),
|
|
46
|
+
operationReceipt: check(
|
|
47
|
+
"workspace_instruction_policy_revisions_operation_receipt_chk",
|
|
48
|
+
sql`(
|
|
49
|
+
(${table.operationId} is null and ${table.requestFingerprint} is null)
|
|
50
|
+
or (
|
|
51
|
+
${table.operationId} is not null
|
|
52
|
+
and ${table.requestFingerprint} ~ '^[0-9a-f]{64}$'
|
|
53
|
+
)
|
|
54
|
+
)`,
|
|
55
|
+
),
|
|
41
56
|
workspaceRevision: uniqueIndex(
|
|
42
57
|
"workspace_instruction_policy_revisions_workspace_revision_uq",
|
|
43
58
|
).on(table.workspaceId, table.revision),
|
|
@@ -99,6 +114,8 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
|
|
|
99
114
|
"workspace_instruction_policy_activation_events",
|
|
100
115
|
{
|
|
101
116
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
117
|
+
operationId: uuid("operation_id"),
|
|
118
|
+
requestFingerprint: text("request_fingerprint"),
|
|
102
119
|
accountId: uuid("account_id").notNull(),
|
|
103
120
|
workspaceId: uuid("workspace_id").notNull(),
|
|
104
121
|
kind: text("kind").notNull(),
|
|
@@ -117,6 +134,19 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
|
|
|
117
134
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
118
135
|
},
|
|
119
136
|
(table) => ({
|
|
137
|
+
workspaceOperation: uniqueIndex("workspace_instruction_policy_events_workspace_operation_uq")
|
|
138
|
+
.on(table.workspaceId, table.operationId)
|
|
139
|
+
.where(sql`${table.operationId} is not null`),
|
|
140
|
+
operationReceipt: check(
|
|
141
|
+
"workspace_instruction_policy_events_operation_receipt_chk",
|
|
142
|
+
sql`(
|
|
143
|
+
(${table.operationId} is null and ${table.requestFingerprint} is null)
|
|
144
|
+
or (
|
|
145
|
+
${table.operationId} is not null
|
|
146
|
+
and ${table.requestFingerprint} ~ '^[0-9a-f]{64}$'
|
|
147
|
+
)
|
|
148
|
+
)`,
|
|
149
|
+
),
|
|
120
150
|
workspaceActivationVersion: uniqueIndex(
|
|
121
151
|
"workspace_instruction_policy_events_target_version_uq",
|
|
122
152
|
).on(
|
|
@@ -142,6 +172,92 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
|
|
|
142
172
|
}),
|
|
143
173
|
);
|
|
144
174
|
|
|
175
|
+
export const workspaceInstructionPolicyOnboardingProposals = pgTable(
|
|
176
|
+
"workspace_instruction_policy_onboarding_proposals",
|
|
177
|
+
{
|
|
178
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
179
|
+
operationId: uuid("operation_id").notNull(),
|
|
180
|
+
requestFingerprint: text("request_fingerprint").notNull(),
|
|
181
|
+
accountId: uuid("account_id").notNull(),
|
|
182
|
+
workspaceId: uuid("workspace_id").notNull(),
|
|
183
|
+
kind: text("kind").notNull(),
|
|
184
|
+
scope: text("scope").notNull(),
|
|
185
|
+
roleKey: text("role_key"),
|
|
186
|
+
sourceId: text("source_id").notNull(),
|
|
187
|
+
sourceVersion: text("source_version").notNull(),
|
|
188
|
+
confidenceBps: integer("confidence_bps").notNull(),
|
|
189
|
+
baselineRevisionId: uuid("baseline_revision_id"),
|
|
190
|
+
baselineRevision: bigint("baseline_revision", { mode: "number" }),
|
|
191
|
+
baselineContentHash: text("baseline_content_hash"),
|
|
192
|
+
baselineActivationVersion: bigint("baseline_activation_version", { mode: "number" }).notNull(),
|
|
193
|
+
baselineActivatedAt: timestamp("baseline_activated_at", { withTimezone: true }),
|
|
194
|
+
draftRevisionId: uuid("draft_revision_id").notNull(),
|
|
195
|
+
draftRevision: bigint("draft_revision", { mode: "number" }).notNull(),
|
|
196
|
+
draftContentHash: text("draft_content_hash").notNull(),
|
|
197
|
+
status: text("status").notNull().default("proposed"),
|
|
198
|
+
createdBySubjectId: text("created_by_subject_id").notNull(),
|
|
199
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
200
|
+
},
|
|
201
|
+
(table) => ({
|
|
202
|
+
workspaceOperation: uniqueIndex(
|
|
203
|
+
"workspace_instruction_policy_onboarding_proposals_workspace_operation_uq",
|
|
204
|
+
).on(table.workspaceId, table.operationId),
|
|
205
|
+
sourceVersionTarget: uniqueIndex(
|
|
206
|
+
"workspace_instruction_policy_onboarding_proposals_source_version_target_uq",
|
|
207
|
+
).on(
|
|
208
|
+
table.workspaceId,
|
|
209
|
+
table.kind,
|
|
210
|
+
table.scope,
|
|
211
|
+
sql`coalesce(${table.roleKey}, '')`,
|
|
212
|
+
table.sourceId,
|
|
213
|
+
table.sourceVersion,
|
|
214
|
+
),
|
|
215
|
+
workspaceTimeline: index(
|
|
216
|
+
"workspace_instruction_policy_onboarding_proposals_workspace_time_idx",
|
|
217
|
+
).on(table.workspaceId, table.createdAt, table.id),
|
|
218
|
+
operationReceipt: check(
|
|
219
|
+
"workspace_instruction_policy_onboarding_proposals_operation_receipt_chk",
|
|
220
|
+
sql`${table.requestFingerprint} ~ '^[0-9a-f]{64}$'`,
|
|
221
|
+
),
|
|
222
|
+
target: check(
|
|
223
|
+
"workspace_instruction_policy_onboarding_proposals_target_chk",
|
|
224
|
+
sql`(
|
|
225
|
+
(${table.kind} = 'charter' and ${table.scope} = 'global' and ${table.roleKey} is null)
|
|
226
|
+
or (${table.kind} = 'policy' and ${table.scope} = 'global' and ${table.roleKey} is null)
|
|
227
|
+
or (${table.kind} = 'policy' and ${table.scope} = 'role' and ${table.roleKey} is not null)
|
|
228
|
+
)`,
|
|
229
|
+
),
|
|
230
|
+
source: check(
|
|
231
|
+
"workspace_instruction_policy_onboarding_proposals_source_chk",
|
|
232
|
+
sql`length(btrim(${table.sourceId})) between 1 and 512
|
|
233
|
+
and length(btrim(${table.sourceVersion})) between 1 and 256
|
|
234
|
+
and ${table.confidenceBps} between 0 and 10000`,
|
|
235
|
+
),
|
|
236
|
+
baseline: check(
|
|
237
|
+
"workspace_instruction_policy_onboarding_proposals_baseline_chk",
|
|
238
|
+
sql`(
|
|
239
|
+
${table.baselineRevisionId} is null
|
|
240
|
+
and ${table.baselineRevision} is null
|
|
241
|
+
and ${table.baselineContentHash} is null
|
|
242
|
+
and ${table.baselineActivationVersion} = 0
|
|
243
|
+
and ${table.baselineActivatedAt} is null
|
|
244
|
+
) or (
|
|
245
|
+
${table.baselineRevisionId} is not null
|
|
246
|
+
and ${table.baselineRevision} > 0
|
|
247
|
+
and ${table.baselineContentHash} ~ '^[0-9a-f]{64}$'
|
|
248
|
+
and ${table.baselineActivationVersion} > 0
|
|
249
|
+
and ${table.baselineActivatedAt} is not null
|
|
250
|
+
)`,
|
|
251
|
+
),
|
|
252
|
+
draft: check(
|
|
253
|
+
"workspace_instruction_policy_onboarding_proposals_draft_chk",
|
|
254
|
+
sql`${table.draftRevision} > 0
|
|
255
|
+
and ${table.draftContentHash} ~ '^[0-9a-f]{64}$'
|
|
256
|
+
and ${table.status} = 'proposed'`,
|
|
257
|
+
),
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
145
261
|
export const workspaceInstructionPolicySnapshots = pgTable(
|
|
146
262
|
"workspace_instruction_policy_snapshots",
|
|
147
263
|
{
|