@opengeni/sdk 4.4.1 → 4.5.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.
Files changed (57) hide show
  1. package/README.md +27 -0
  2. package/dist/adapters/next.js +2 -2
  3. package/dist/artifacts.js +4 -4
  4. package/dist/browser.d.ts +2 -0
  5. package/dist/browser.js +5 -3
  6. package/dist/chat/index.js +5 -5
  7. package/dist/{chunk-3BWTS2MJ.js → chunk-ARDVIZIJ.js} +2 -2
  8. package/dist/{chunk-PVWOBYAZ.js → chunk-BXMNDWAN.js} +256 -78
  9. package/dist/chunk-BXMNDWAN.js.map +1 -0
  10. package/dist/{chunk-IVHLADZ7.js → chunk-GRUMGFK6.js} +2 -2
  11. package/dist/{chunk-2RVPHF37.js → chunk-JBAGMSN2.js} +66 -10
  12. package/dist/chunk-JBAGMSN2.js.map +1 -0
  13. package/dist/{chunk-35S72HAE.js → chunk-U4JUWOZV.js} +5 -5
  14. package/dist/{chunk-BLID3DCD.js → chunk-V4AIZAFF.js} +1 -1
  15. package/dist/chunk-V4AIZAFF.js.map +1 -0
  16. package/dist/{chunk-ALXDFL6C.js → chunk-VSOXNL3T.js} +2 -2
  17. package/dist/{chunk-CT6P2W2C.js → chunk-WI3WUT5H.js} +2 -2
  18. package/dist/client.d.ts +24 -1
  19. package/dist/codex-realtime-controller.js +1 -1
  20. package/dist/core.d.ts +2 -0
  21. package/dist/core.js +5 -3
  22. package/dist/document-authority.js +3 -3
  23. package/dist/editable-artifacts.js +1 -1
  24. package/dist/index.d.ts +4 -2
  25. package/dist/index.js +35 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/read-retry.d.ts +61 -0
  28. package/dist/realtime.js +5 -5
  29. package/dist/session-proxy.d.ts +6 -1
  30. package/dist/session-proxy.js +2 -2
  31. package/dist/site-bridge-host.js +3 -3
  32. package/dist/site-browser-runtime.gen.d.ts +1 -1
  33. package/dist/site-document.js +1 -1
  34. package/dist/site-document.js.map +1 -1
  35. package/dist/site.js +3 -3
  36. package/dist/types.d.ts +40 -0
  37. package/dist/workspace-artifacts.d.ts +5 -0
  38. package/dist/workspace-live-stream.d.ts +7 -0
  39. package/package.json +3 -3
  40. package/src/browser.ts +2 -0
  41. package/src/client.ts +191 -73
  42. package/src/core.ts +2 -0
  43. package/src/index.ts +4 -0
  44. package/src/read-retry.ts +207 -0
  45. package/src/session-proxy.ts +95 -4
  46. package/src/site-browser-runtime.gen.ts +1 -1
  47. package/src/types.ts +38 -0
  48. package/src/workspace-artifacts.ts +6 -0
  49. package/src/workspace-live-stream.ts +12 -1
  50. package/dist/chunk-2RVPHF37.js.map +0 -1
  51. package/dist/chunk-BLID3DCD.js.map +0 -1
  52. package/dist/chunk-PVWOBYAZ.js.map +0 -1
  53. /package/dist/{chunk-3BWTS2MJ.js.map → chunk-ARDVIZIJ.js.map} +0 -0
  54. /package/dist/{chunk-IVHLADZ7.js.map → chunk-GRUMGFK6.js.map} +0 -0
  55. /package/dist/{chunk-35S72HAE.js.map → chunk-U4JUWOZV.js.map} +0 -0
  56. /package/dist/{chunk-ALXDFL6C.js.map → chunk-VSOXNL3T.js.map} +0 -0
  57. /package/dist/{chunk-CT6P2W2C.js.map → chunk-WI3WUT5H.js.map} +0 -0
package/src/index.ts CHANGED
@@ -346,6 +346,8 @@ export {
346
346
  isRetryableStreamError,
347
347
  } from "./errors";
348
348
  export type { OpenGeniSecureContextRequiredReason } from "./errors";
349
+ export { isTransientRequestError } from "./read-retry";
350
+ export type { OpenGeniReadRetryOptions } from "./read-retry";
349
351
  export { parseDeprecationNotice } from "./deprecation";
350
352
  export type { OpenGeniDeprecationHandler, OpenGeniDeprecationNotice } from "./deprecation";
351
353
  export {
@@ -532,6 +534,7 @@ export type {
532
534
  PublishWorkspaceArtifactVersionRequest,
533
535
  RollbackWorkspaceArtifactRequest,
534
536
  SetWorkspaceArtifactStatusRequest,
537
+ SiteOpenedResponse,
535
538
  UpdateWorkspaceArtifactVisibilityRequest,
536
539
  UpdateWorkspaceArtifactVisibilityResponse,
537
540
  WorkspaceArtifactVisibility,
@@ -1320,6 +1323,7 @@ export type {
1320
1323
  AgentTopologySession,
1321
1324
  SessionListResponse,
1322
1325
  SessionListTotals,
1326
+ SiteConversationGroup,
1323
1327
  SessionListEntry,
1324
1328
  SessionListEntryResponse,
1325
1329
  SessionLineageResponse,
@@ -0,0 +1,207 @@
1
+ import { OpenGeniApiError } from "./errors";
2
+
3
+ /**
4
+ * Bounded retry for safe reads (`GET` and `HEAD`) that failed transiently:
5
+ * the request never reached a healthy server (a transport error such as a
6
+ * reset connection) or a gateway answered 502, 503 or 504. These are what an
7
+ * API rolling deploy or a short network blip look like from the client.
8
+ * Mutations are never retried here; they keep their outcome-unknown semantics.
9
+ */
10
+ export type OpenGeniReadRetryOptions = {
11
+ /** Total attempts including the first. Defaults to 4; `1` disables retries. */
12
+ maxAttempts?: number | undefined;
13
+ /** Backoff before the first retry, before jitter. Defaults to 250 ms. Doubles per retry. */
14
+ initialDelayMs?: number | undefined;
15
+ /** Upper bound for one backoff delay. Defaults to 2000 ms. */
16
+ maxDelayMs?: number | undefined;
17
+ /**
18
+ * Total time budget measured from the first attempt. A retry, including one
19
+ * whose `Retry-After` asks for a longer wait, is only made when its delay
20
+ * still fits in the budget; otherwise the last error surfaces. Defaults to 5000 ms.
21
+ */
22
+ maxElapsedMs?: number | undefined;
23
+ };
24
+
25
+ export type ReadRetryPolicy = {
26
+ maxAttempts: number;
27
+ initialDelayMs: number;
28
+ maxDelayMs: number;
29
+ maxElapsedMs: number;
30
+ };
31
+
32
+ export const DEFAULT_READ_RETRY_POLICY: Readonly<ReadRetryPolicy> = Object.freeze({
33
+ maxAttempts: 4,
34
+ initialDelayMs: 250,
35
+ maxDelayMs: 2_000,
36
+ maxElapsedMs: 5_000,
37
+ });
38
+
39
+ const MAX_READ_RETRY_ATTEMPTS = 10;
40
+ const MAX_READ_RETRY_ELAPSED_MS = 60_000;
41
+
42
+ function boundedNumber(value: unknown, fallback: number, min: number, max: number): number {
43
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
44
+ return Math.min(Math.max(value, min), max);
45
+ }
46
+
47
+ /**
48
+ * Resolve client options to a policy, or `null` when retries are off. Invalid
49
+ * values fall back to the defaults instead of failing client construction.
50
+ */
51
+ export function resolveReadRetryPolicy(
52
+ options: OpenGeniReadRetryOptions | false | undefined,
53
+ ): ReadRetryPolicy | null {
54
+ if (options === false) return null;
55
+ const input = options && typeof options === "object" ? options : {};
56
+ const maxAttempts = Math.floor(
57
+ boundedNumber(
58
+ input.maxAttempts,
59
+ DEFAULT_READ_RETRY_POLICY.maxAttempts,
60
+ 1,
61
+ MAX_READ_RETRY_ATTEMPTS,
62
+ ),
63
+ );
64
+ if (maxAttempts <= 1) return null;
65
+ const maxElapsedMs = boundedNumber(
66
+ input.maxElapsedMs,
67
+ DEFAULT_READ_RETRY_POLICY.maxElapsedMs,
68
+ 0,
69
+ MAX_READ_RETRY_ELAPSED_MS,
70
+ );
71
+ const maxDelayMs = boundedNumber(
72
+ input.maxDelayMs,
73
+ DEFAULT_READ_RETRY_POLICY.maxDelayMs,
74
+ 0,
75
+ MAX_READ_RETRY_ELAPSED_MS,
76
+ );
77
+ const initialDelayMs = boundedNumber(
78
+ input.initialDelayMs,
79
+ DEFAULT_READ_RETRY_POLICY.initialDelayMs,
80
+ 0,
81
+ maxDelayMs,
82
+ );
83
+ return { maxAttempts, initialDelayMs, maxDelayMs, maxElapsedMs };
84
+ }
85
+
86
+ export function isSafeReadMethod(method: string): boolean {
87
+ const normalized = method.toUpperCase();
88
+ return normalized === "GET" || normalized === "HEAD";
89
+ }
90
+
91
+ /** Gateway statuses a rolling deploy or an overloaded upstream answers with. */
92
+ export function isTransientGatewayStatus(status: number): boolean {
93
+ return status === 502 || status === 503 || status === 504;
94
+ }
95
+
96
+ // Transport failures the client itself observed (`fetch` or the response body
97
+ // rejected with a `TypeError`). Tagged so a caller can tell them apart from a
98
+ // `TypeError` its own code threw, which is a bug, not a transient failure.
99
+ const transportFailures = new WeakSet<object>();
100
+
101
+ /** Record that `error` is a transport failure of an API request. */
102
+ export function markTransportFailure(error: unknown): void {
103
+ if (error instanceof TypeError) transportFailures.add(error);
104
+ }
105
+
106
+ // 502s the client synthesizes for a response it could not accept (a non-JSON
107
+ // or malformed success body). The server did answer; repeating the read gives
108
+ // the same answer, so these are definitive rather than transient.
109
+ const invalidResponses = new WeakSet<object>();
110
+
111
+ /** A 502 for a response the client received but could not accept. */
112
+ export function invalidApiResponseError(
113
+ body: string,
114
+ options?: ConstructorParameters<typeof OpenGeniApiError>[2],
115
+ ): OpenGeniApiError {
116
+ const error = new OpenGeniApiError(502, body, options);
117
+ invalidResponses.add(error);
118
+ return error;
119
+ }
120
+
121
+ // Transport failure messages across browsers, Node/undici and Bun, for
122
+ // `TypeError`s that did not pass through this client.
123
+ const NETWORK_FAILURE_MESSAGE =
124
+ /failed to fetch|fetch failed|networkerror|network error|load failed|network request failed|network connection was lost|terminated|err_http2|econnreset|socket hang up/iu;
125
+
126
+ /**
127
+ * A failure a later identical safe read can plausibly succeed after: a
128
+ * transport error (`fetch` or the response body rejected with a `TypeError`)
129
+ * or a retryable 502, 503 or 504. Auth, permission, not-found, validation,
130
+ * rate limits, aborts, other server errors, a success response the client
131
+ * could not accept, and `TypeError`s from application code are definitive and
132
+ * are not transient.
133
+ */
134
+ export function isTransientRequestError(error: unknown): boolean {
135
+ if (error instanceof OpenGeniApiError) {
136
+ return (
137
+ !invalidResponses.has(error) && isTransientGatewayStatus(error.status) && error.retryable
138
+ );
139
+ }
140
+ if (!(error instanceof TypeError)) return false;
141
+ return transportFailures.has(error) || NETWORK_FAILURE_MESSAGE.test(error.message);
142
+ }
143
+
144
+ /** `Retry-After` as milliseconds from `now`, or `null` when absent or invalid. */
145
+ export function parseRetryAfterMs(
146
+ value: string | null | undefined,
147
+ now = Date.now(),
148
+ ): number | null {
149
+ if (value === null || value === undefined) return null;
150
+ const trimmed = value.trim();
151
+ if (!trimmed) return null;
152
+ if (/^\d+$/.test(trimmed)) {
153
+ const seconds = Number(trimmed);
154
+ return Number.isSafeInteger(seconds) ? seconds * 1_000 : null;
155
+ }
156
+ // Otherwise only an HTTP-date (it always names the month); never a number.
157
+ if (!/[A-Za-z]/.test(trimmed)) return null;
158
+ const at = Date.parse(trimmed);
159
+ if (!Number.isFinite(at)) return null;
160
+ return Math.max(0, at - now);
161
+ }
162
+
163
+ /**
164
+ * The delay before retry number `retry` (1 for the first retry), or `null`
165
+ * when no retry fits the policy. A server `Retry-After` is honored, plus up to
166
+ * 20% so clients told the same instant do not all return at once; otherwise
167
+ * the exponential backoff is jittered to between half and all of it.
168
+ */
169
+ export function readRetryDelayMs(
170
+ policy: ReadRetryPolicy,
171
+ retry: number,
172
+ elapsedMs: number,
173
+ retryAfterMs: number | null,
174
+ random: () => number = Math.random,
175
+ ): number | null {
176
+ if (retry >= policy.maxAttempts) return null;
177
+ let delay: number;
178
+ if (retryAfterMs !== null) {
179
+ delay = retryAfterMs * (1 + 0.2 * Math.min(Math.max(random(), 0), 1));
180
+ } else {
181
+ const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * 2 ** (retry - 1));
182
+ delay = base / 2 + (base / 2) * Math.min(Math.max(random(), 0), 1);
183
+ }
184
+ if (elapsedMs + delay > policy.maxElapsedMs) return null;
185
+ return Math.round(delay);
186
+ }
187
+
188
+ /** Resolve after `ms`, or reject with the signal's reason once it aborts. */
189
+ export function abortableReadRetryDelay(
190
+ ms: number,
191
+ signal: AbortSignal | undefined,
192
+ ): Promise<void> {
193
+ if (signal?.aborted) {
194
+ return Promise.reject(signal.reason ?? new DOMException("Request aborted", "AbortError"));
195
+ }
196
+ return new Promise((resolve, reject) => {
197
+ const onAbort = () => {
198
+ clearTimeout(timer);
199
+ reject(signal?.reason ?? new DOMException("Request aborted", "AbortError"));
200
+ };
201
+ const timer = setTimeout(() => {
202
+ signal?.removeEventListener("abort", onAbort);
203
+ resolve();
204
+ }, ms);
205
+ signal?.addEventListener("abort", onAbort, { once: true });
206
+ });
207
+ }
@@ -95,7 +95,9 @@ export type SessionProxyContext = {
95
95
  };
96
96
 
97
97
  /**
98
- * The only fields a browser may send when creating a session through the proxy.
98
+ * The only fields a browser may send when creating a session through the proxy,
99
+ * besides `modelContext`, which the proxy appends after any context the hook
100
+ * and `messageExtras` return, as it does for follow-up messages.
99
101
  * After the `createSession` hook returns, the proxy adds the attached files to
100
102
  * the created request (deduplicated by file id) and, when `modelSelection` is
101
103
  * not `false`, applies the browser's explicit model choices over the hook's,
@@ -358,6 +360,9 @@ export type SessionProxyHandlerOptions = {
358
360
  * pinning a chat, moving it to a project, managing projects and a chat's
359
361
  * tool approval settings. Every other write is refused.
360
362
  * - Catalog logos are served from the Opengeni API.
363
+ * - Creating a chat also takes the app's own create fields: the id of its
364
+ * first message and the new-chat draft revision it sent. It sends
365
+ * `visibility` and `connectionAccounts` too; the server chooses both.
361
366
  */
362
367
  sessionApp?: boolean | undefined;
363
368
  /**
@@ -406,6 +411,14 @@ const CREATE_FIELDS: ReadonlySet<string> = new Set([
406
411
  "idempotencyKey",
407
412
  "resources",
408
413
  "startMode",
414
+ "modelContext",
415
+ ]);
416
+ /** Extra fields the packaged session app sends on create (see `browserCreateFields`). */
417
+ const SESSION_APP_CREATE_FIELDS: ReadonlySet<string> = new Set([
418
+ "visibility",
419
+ "clientEventId",
420
+ "connectionAccounts",
421
+ "expectedNewSessionDraftRevision",
409
422
  ]);
410
423
  const MODEL_FIELDS = ["model", "reasoningEffort", "latencyMode"] as const;
411
424
  /** The API's single-recording ceiling (25 MiB) plus multipart framing. */
@@ -1148,7 +1161,9 @@ export function createSessionProxyHandler(
1148
1161
  if (method !== "POST" || !options.createSession) {
1149
1162
  return errorJson(404, "route_not_allowed", "Not found.");
1150
1163
  }
1151
- const input = createInput(await readJsonBody(request, maxBodyBytes), modelSelection);
1164
+ const body = await readJsonBody(request, maxBodyBytes);
1165
+ const input = createInput(body, modelSelection, sessionAppEnabled);
1166
+ const browser = browserCreateFields(body, sessionAppEnabled);
1152
1167
  if (input.startMode === "realtime" && !realtimeVoiceEnabled) {
1153
1168
  return errorJson(404, "route_not_allowed", "Not found.");
1154
1169
  }
@@ -1157,13 +1172,18 @@ export function createSessionProxyHandler(
1157
1172
  const created = withBrowserCreateChoices(
1158
1173
  toolServer ? withToolServer(hooked, toolServer, await toolToken()) : hooked,
1159
1174
  input,
1175
+ browser,
1160
1176
  );
1161
1177
  const extras = await messageExtras({
1162
1178
  delivery: "create",
1163
1179
  ...(input.startMode ? { startMode: input.startMode } : {}),
1164
1180
  });
1165
1181
  if (extras instanceof Response) return extras;
1166
- const modelContext = joinContext(extras?.modelContext, created.modelContext);
1182
+ const modelContext = joinContext(
1183
+ extras?.modelContext,
1184
+ created.modelContext,
1185
+ browser.modelContext,
1186
+ );
1167
1187
  return json(
1168
1188
  await client.createSession(workspaceId, {
1169
1189
  ...created,
@@ -2015,9 +2035,14 @@ function pick(
2015
2035
  function createInput(
2016
2036
  body: Record<string, unknown> | undefined,
2017
2037
  modelSelection: boolean,
2038
+ sessionApp: boolean,
2018
2039
  ): SessionProxyCreateInput {
2019
2040
  for (const key of Object.keys(body ?? {})) {
2020
- if (!CREATE_FIELDS.has(key) && !(modelSelection && isModelField(key))) {
2041
+ if (
2042
+ !CREATE_FIELDS.has(key) &&
2043
+ !(modelSelection && isModelField(key)) &&
2044
+ !(sessionApp && SESSION_APP_CREATE_FIELDS.has(key))
2045
+ ) {
2021
2046
  reject(
2022
2047
  400,
2023
2048
  "create_field_not_allowed",
@@ -2097,9 +2122,68 @@ function createResources(value: unknown): FileResourceRef[] {
2097
2122
  }
2098
2123
 
2099
2124
  /** Browser attachments are added; explicit browser model choices win, as for messages. */
2125
+ type BrowserCreateFields = {
2126
+ modelContext?: string | undefined;
2127
+ clientEventId?: string | undefined;
2128
+ expectedNewSessionDraftRevision?: number | undefined;
2129
+ };
2130
+
2131
+ /**
2132
+ * The browser's own context and the session app's create bookkeeping. Message
2133
+ * context is appended after the server's, as on follow-up messages. The session
2134
+ * app also sends the id of its first message event and the revision of the
2135
+ * new-chat draft it sent, so the API can clear that draft. It sends `visibility`
2136
+ * and an empty `connectionAccounts` too: the server chooses both, so those are
2137
+ * dropped, and any account selection is refused.
2138
+ */
2139
+ function browserCreateFields(
2140
+ body: Record<string, unknown> | undefined,
2141
+ sessionApp: boolean,
2142
+ ): BrowserCreateFields {
2143
+ const fields: BrowserCreateFields = {};
2144
+ const modelContext = body?.modelContext;
2145
+ if (modelContext !== undefined && typeof modelContext !== "string") {
2146
+ reject(400, "invalid_model_context", "modelContext must be a string.");
2147
+ }
2148
+ if (modelContext?.trim()) fields.modelContext = modelContext;
2149
+ if (!sessionApp) return fields;
2150
+ const visibility = body?.visibility;
2151
+ if (visibility !== undefined && visibility !== "workspace" && visibility !== "private") {
2152
+ reject(400, "invalid_visibility", "visibility must be workspace or private.");
2153
+ }
2154
+ const accounts = body?.connectionAccounts;
2155
+ if (accounts !== undefined && (!Array.isArray(accounts) || accounts.length > 0)) {
2156
+ reject(
2157
+ 403,
2158
+ "connection_account_not_allowed",
2159
+ "The server chooses which connected accounts a chat uses.",
2160
+ );
2161
+ }
2162
+ const clientEventId = body?.clientEventId;
2163
+ if (clientEventId !== undefined) {
2164
+ if (typeof clientEventId !== "string" || !clientEventId) {
2165
+ reject(400, "invalid_client_event_id", "clientEventId must be a non-empty string.");
2166
+ }
2167
+ fields.clientEventId = clientEventId;
2168
+ }
2169
+ const revision = body?.expectedNewSessionDraftRevision;
2170
+ if (revision !== undefined) {
2171
+ if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 0) {
2172
+ reject(
2173
+ 400,
2174
+ "invalid_draft_revision",
2175
+ "expectedNewSessionDraftRevision must be a non-negative integer.",
2176
+ );
2177
+ }
2178
+ fields.expectedNewSessionDraftRevision = revision;
2179
+ }
2180
+ return fields;
2181
+ }
2182
+
2100
2183
  function withBrowserCreateChoices(
2101
2184
  created: CreateSessionRequest,
2102
2185
  input: SessionProxyCreateInput,
2186
+ browser: BrowserCreateFields = {},
2103
2187
  ): CreateSessionRequest {
2104
2188
  const existing = created.resources ?? [];
2105
2189
  const attached = new Set(
@@ -2118,6 +2202,13 @@ function withBrowserCreateChoices(
2118
2202
  ...(input.model ? { model: input.model } : {}),
2119
2203
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
2120
2204
  ...(input.latencyMode ? { latencyMode: input.latencyMode } : {}),
2205
+ ...(created.clientEventId === undefined && browser.clientEventId
2206
+ ? { clientEventId: browser.clientEventId }
2207
+ : {}),
2208
+ ...(created.expectedNewSessionDraftRevision === undefined &&
2209
+ browser.expectedNewSessionDraftRevision !== undefined
2210
+ ? { expectedNewSessionDraftRevision: browser.expectedNewSessionDraftRevision }
2211
+ : {}),
2121
2212
  };
2122
2213
  }
2123
2214