@opengeni/sdk 0.13.0 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Framework-agnostic TypeScript SDK for the OpenGeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/client.ts CHANGED
@@ -1,9 +1,13 @@
1
- import { OpenGeniApiError } from "./errors";
1
+ import { OpenGeniApiContractMismatchError, OpenGeniApiError } from "./errors";
2
2
  import {
3
3
  streamSessionEvents,
4
4
  type SessionEventStreamTransport,
5
5
  type StreamSessionEventsOptions,
6
6
  } from "./stream";
7
+ import {
8
+ streamWorkspaceControlEvents,
9
+ type WorkspaceControlStreamTransport,
10
+ } from "./workspace-control-stream";
7
11
  import type {
8
12
  AccessContext,
9
13
  AddWorkspaceMemberRequest,
@@ -90,8 +94,15 @@ import type {
90
94
  SessionMcpCredentialUpdateInput,
91
95
  SessionQueueSnapshot,
92
96
  SessionQueueMutationResponse,
97
+ ComposerDraft,
98
+ DeleteSessionQueueItemRequest,
99
+ EditSessionQueueItemRequest,
100
+ MoveSessionQueueItemRequest,
101
+ SaveComposerDraftRequest,
102
+ SteerSessionQueueItemRequest,
93
103
  SessionControlResponse,
94
104
  WorkspaceInferenceControlResponse,
105
+ WorkspaceControlEvent,
95
106
  SessionTurn,
96
107
  // Stream surfacing (Phase 5): capability negotiation + viewer lifecycle + config.
97
108
  SessionCapabilities,
@@ -161,6 +172,7 @@ import type {
161
172
  OAuthStartRequest,
162
173
  OAuthStartResponse,
163
174
  } from "./types";
175
+ import { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "./types";
164
176
 
165
177
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
166
178
 
@@ -182,8 +194,8 @@ export type SendMessageInput = {
182
194
  model?: string;
183
195
  reasoningEffort?: ReasoningEffort;
184
196
  clientEventId?: string;
185
- expectedControlGeneration?: number;
186
- expectedWorkspaceInferenceGeneration?: number;
197
+ controlEtag?: string;
198
+ expectedDraftRevision?: number;
187
199
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
188
200
  };
189
201
 
@@ -276,7 +288,7 @@ export class OpenGeniClient {
276
288
  search?: string;
277
289
  } = {},
278
290
  ): Promise<SessionListResponse> {
279
- const response = await this.requestJson<SessionListResponse | Session[]>(
291
+ return await this.requestJson<SessionListResponse>(
280
292
  "GET",
281
293
  `/v1/workspaces/${workspaceId}/sessions`,
282
294
  undefined,
@@ -294,23 +306,6 @@ export class OpenGeniClient {
294
306
  : {}),
295
307
  },
296
308
  );
297
- if (Array.isArray(response)) {
298
- // Rolling/same-major compatibility: an older API ignores `view=page` and
299
- // returns the historical array. That is an honest one-page projection;
300
- // never pretend it honored a cursor supplied directly by a caller.
301
- if (options.cursor) {
302
- throw new Error("The connected OpenGeni API does not support stable session-page cursors");
303
- }
304
- // Older APIs ignore unknown query parameters. Treating their unfiltered
305
- // array as a successful search would be worse than an explicit rolling-
306
- // upgrade error (and client-side filtering cannot recover matches beyond
307
- // the old endpoint's bounded first page).
308
- if (options.search?.trim()) {
309
- throw new Error("The connected OpenGeni API does not support session search");
310
- }
311
- return { pinned: [], sessions: response, nextCursor: null };
312
- }
313
- return response;
314
309
  }
315
310
 
316
311
  /** Set this authenticated member's personal workspace pin for a session. */
@@ -551,14 +546,14 @@ export class OpenGeniClient {
551
546
  async pauseSession(
552
547
  workspaceId: string,
553
548
  sessionId: string,
554
- options: { reason?: string; clientEventId?: string } = {},
555
- ): Promise<SessionEvent> {
556
- return (
557
- await this.controlSession(workspaceId, sessionId, {
558
- mode: "pause",
559
- ...options,
560
- })
561
- ).event;
549
+ options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
550
+ ): Promise<SessionControlResponse> {
551
+ return await this.controlSession(workspaceId, sessionId, {
552
+ action: "pause",
553
+ clientEventId: options.clientEventId ?? crypto.randomUUID(),
554
+ ...(options.reason ? { reason: options.reason } : {}),
555
+ ...(options.expectedControlEtag ? { expectedControlEtag: options.expectedControlEtag } : {}),
556
+ });
562
557
  }
563
558
 
564
559
  async sendApprovalDecision(
@@ -619,6 +614,7 @@ export class OpenGeniClient {
619
614
  headers: { ...this.headers(), Accept: "text/event-stream" },
620
615
  ...(options.signal ? { signal: options.signal } : {}),
621
616
  });
617
+ assertApiContractResponse(response);
622
618
  if (!response.ok) {
623
619
  throw new OpenGeniApiError(response.status, await safeText(response));
624
620
  }
@@ -637,15 +633,73 @@ export class OpenGeniClient {
637
633
  );
638
634
  }
639
635
 
640
- async cancelQueueItem(
636
+ async moveQueueItem(
637
+ workspaceId: string,
638
+ sessionId: string,
639
+ turnId: string,
640
+ request: MoveSessionQueueItemRequest,
641
+ ): Promise<SessionQueueMutationResponse> {
642
+ return await this.requestJson<SessionQueueMutationResponse>(
643
+ "POST",
644
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/move`,
645
+ request,
646
+ );
647
+ }
648
+
649
+ async editQueueItem(
650
+ workspaceId: string,
651
+ sessionId: string,
652
+ turnId: string,
653
+ request: EditSessionQueueItemRequest,
654
+ ): Promise<SessionQueueMutationResponse> {
655
+ return await this.requestJson<SessionQueueMutationResponse>(
656
+ "POST",
657
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/edit`,
658
+ request,
659
+ );
660
+ }
661
+
662
+ async steerQueueItem(
663
+ workspaceId: string,
664
+ sessionId: string,
665
+ turnId: string,
666
+ request: SteerSessionQueueItemRequest,
667
+ ): Promise<SessionQueueMutationResponse> {
668
+ return await this.requestJson<SessionQueueMutationResponse>(
669
+ "POST",
670
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/steer`,
671
+ request,
672
+ );
673
+ }
674
+
675
+ async deleteQueueItem(
641
676
  workspaceId: string,
642
677
  sessionId: string,
643
678
  turnId: string,
644
- request: { expectedQueueVersion: number; expectedItemVersion: number; reason?: string },
679
+ request: DeleteSessionQueueItemRequest,
645
680
  ): Promise<SessionQueueMutationResponse> {
646
681
  return await this.requestJson<SessionQueueMutationResponse>(
647
682
  "POST",
648
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/cancel`,
683
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/delete`,
684
+ request,
685
+ );
686
+ }
687
+
688
+ async getComposerDraft(workspaceId: string, sessionId: string): Promise<ComposerDraft> {
689
+ return await this.requestJson<ComposerDraft>(
690
+ "GET",
691
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/composer-draft`,
692
+ );
693
+ }
694
+
695
+ async saveComposerDraft(
696
+ workspaceId: string,
697
+ sessionId: string,
698
+ request: SaveComposerDraftRequest,
699
+ ): Promise<ComposerDraft> {
700
+ return await this.requestJson<ComposerDraft>(
701
+ "PUT",
702
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/composer-draft`,
649
703
  request,
650
704
  );
651
705
  }
@@ -654,12 +708,10 @@ export class OpenGeniClient {
654
708
  workspaceId: string,
655
709
  sessionId: string,
656
710
  request: {
657
- mode: "pause" | "resume";
711
+ action: "pause" | "resume";
658
712
  reason?: string;
659
- clientEventId?: string;
660
- expectedControlState?: "active" | "paused";
661
- expectedControlGeneration?: number;
662
- expectedWorkspaceInferenceGeneration?: number;
713
+ clientEventId: string;
714
+ expectedControlEtag?: string;
663
715
  },
664
716
  ): Promise<SessionControlResponse> {
665
717
  return await this.requestJson<SessionControlResponse>(
@@ -672,20 +724,23 @@ export class OpenGeniClient {
672
724
  async resumeSession(
673
725
  workspaceId: string,
674
726
  sessionId: string,
675
- options: { reason?: string; clientEventId?: string } = {},
727
+ options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
676
728
  ): Promise<SessionControlResponse> {
677
- return await this.controlSession(workspaceId, sessionId, { mode: "resume", ...options });
729
+ return await this.controlSession(workspaceId, sessionId, {
730
+ action: "resume",
731
+ clientEventId: options.clientEventId ?? crypto.randomUUID(),
732
+ ...(options.reason ? { reason: options.reason } : {}),
733
+ ...(options.expectedControlEtag ? { expectedControlEtag: options.expectedControlEtag } : {}),
734
+ });
678
735
  }
679
736
 
680
737
  async setWorkspaceInferenceState(
681
738
  workspaceId: string,
682
739
  request: {
683
- state: "active" | "paused";
684
- reason: string;
740
+ action: "pause" | "resume";
741
+ reason?: string;
685
742
  clientEventId: string;
686
- expectedState: "active" | "paused";
687
- expectedGeneration: number;
688
- exceptSessionIds?: string[];
743
+ expectedRevision?: number;
689
744
  },
690
745
  ): Promise<WorkspaceInferenceControlResponse> {
691
746
  return await this.requestJson<WorkspaceInferenceControlResponse>(
@@ -695,18 +750,60 @@ export class OpenGeniClient {
695
750
  );
696
751
  }
697
752
 
698
- /** Cancel a queued turn before it is claimed. Returns the cancelled turn. */
699
- async deleteQueuedTurn(
753
+ async listWorkspaceControlEvents(
700
754
  workspaceId: string,
701
- sessionId: string,
702
- turnId: string,
703
- ): Promise<SessionTurn> {
704
- return await this.requestJson<SessionTurn>(
705
- "DELETE",
706
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
755
+ options: { after?: number; limit?: number } = {},
756
+ ): Promise<WorkspaceControlEvent[]> {
757
+ return await this.requestJson<WorkspaceControlEvent[]>(
758
+ "GET",
759
+ `/v1/workspaces/${workspaceId}/control-events`,
760
+ undefined,
761
+ {
762
+ ...(options.after !== undefined ? { after: String(options.after) } : {}),
763
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
764
+ },
707
765
  );
708
766
  }
709
767
 
768
+ streamWorkspaceControlEvents(
769
+ workspaceId: string,
770
+ options: StreamSessionEventsOptions = {},
771
+ ): AsyncGenerator<WorkspaceControlEvent, void, void> {
772
+ return streamWorkspaceControlEvents(this.workspaceControlStreamTransport(workspaceId), options);
773
+ }
774
+
775
+ workspaceControlStreamTransport(workspaceId: string): WorkspaceControlStreamTransport {
776
+ return {
777
+ openStream: async (after, signal) =>
778
+ await this.openWorkspaceControlEventStream(workspaceId, {
779
+ after,
780
+ ...(signal ? { signal } : {}),
781
+ }),
782
+ };
783
+ }
784
+
785
+ async openWorkspaceControlEventStream(
786
+ workspaceId: string,
787
+ options: { after?: number; signal?: AbortSignal } = {},
788
+ ): Promise<ReadableStream<Uint8Array>> {
789
+ const response = await this.fetchImpl(
790
+ this.url(`/v1/workspaces/${workspaceId}/control-events/stream`, {
791
+ after: String(options.after ?? 0),
792
+ }),
793
+ {
794
+ method: "GET",
795
+ headers: { ...this.headers(), Accept: "text/event-stream" },
796
+ ...(options.signal ? { signal: options.signal } : {}),
797
+ },
798
+ );
799
+ assertApiContractResponse(response);
800
+ if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
801
+ if (!response.body) {
802
+ throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
803
+ }
804
+ return response.body;
805
+ }
806
+
710
807
  /**
711
808
  * Steer: atomically put this prompt at the head and supersede the current
712
809
  * inference. The client performs one request and renders server order.
@@ -1121,7 +1218,14 @@ export class OpenGeniClient {
1121
1218
  * knowledge of the host setup; safe to call before any auth is established.
1122
1219
  */
1123
1220
  async getClientConfig(): Promise<ClientConfig> {
1124
- return await this.requestJson<ClientConfig>("GET", "/v1/config/client");
1221
+ const config = await this.requestJson<ClientConfig>("GET", "/v1/config/client");
1222
+ if (config.apiContractRevision !== OPENGENI_API_CONTRACT_REVISION) {
1223
+ throw new OpenGeniApiContractMismatchError(
1224
+ OPENGENI_API_CONTRACT_REVISION,
1225
+ String(config.apiContractRevision || "(missing)"),
1226
+ );
1227
+ }
1228
+ return config;
1125
1229
  }
1126
1230
 
1127
1231
  /** The caller's access context: subject, account + workspace grants, defaults. */
@@ -2058,6 +2162,7 @@ export class OpenGeniClient {
2058
2162
  return {
2059
2163
  ...(this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}),
2060
2164
  ...extra,
2165
+ [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
2061
2166
  };
2062
2167
  }
2063
2168
 
@@ -2208,6 +2313,7 @@ export class OpenGeniClient {
2208
2313
  },
2209
2314
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2210
2315
  });
2316
+ assertApiContractResponse(response);
2211
2317
  if (!response.ok) {
2212
2318
  throw new OpenGeniApiError(response.status, await safeText(response));
2213
2319
  }
@@ -2225,12 +2331,20 @@ export class OpenGeniClient {
2225
2331
  },
2226
2332
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
2227
2333
  });
2334
+ assertApiContractResponse(response);
2228
2335
  if (!response.ok) {
2229
2336
  throw new OpenGeniApiError(response.status, await safeText(response));
2230
2337
  }
2231
2338
  }
2232
2339
  }
2233
2340
 
2341
+ function assertApiContractResponse(response: Response): void {
2342
+ const actual = response.headers.get(OPENGENI_API_CONTRACT_HEADER);
2343
+ if (actual && actual !== OPENGENI_API_CONTRACT_REVISION) {
2344
+ throw new OpenGeniApiContractMismatchError(OPENGENI_API_CONTRACT_REVISION, actual);
2345
+ }
2346
+ }
2347
+
2234
2348
  async function safeText(response: Response): Promise<string> {
2235
2349
  try {
2236
2350
  return await response.text();
package/src/errors.ts CHANGED
@@ -11,6 +11,19 @@ export class OpenGeniApiError extends Error {
11
11
  }
12
12
  }
13
13
 
14
+ /** The browser bundle and API disagree about their state-changing wire contract. */
15
+ export class OpenGeniApiContractMismatchError extends Error {
16
+ readonly expected: string;
17
+ readonly actual: string;
18
+
19
+ constructor(expected: string, actual: string) {
20
+ super(`OpenGeni API contract mismatch: client expects ${expected}, API serves ${actual}`);
21
+ this.name = "OpenGeniApiContractMismatchError";
22
+ this.expected = expected;
23
+ this.actual = actual;
24
+ }
25
+ }
26
+
14
27
  /** Error for an unrecoverable event-stream condition (not a transient drop). */
15
28
  export class OpenGeniStreamError extends Error {
16
29
  constructor(message: string) {
package/src/index.ts CHANGED
@@ -5,7 +5,12 @@ export type {
5
5
  SendMessageInput,
6
6
  SteerMessageResult,
7
7
  } from "./client";
8
- export { OpenGeniApiError, OpenGeniStreamError, isRetryableStreamError } from "./errors";
8
+ export {
9
+ OpenGeniApiContractMismatchError,
10
+ OpenGeniApiError,
11
+ OpenGeniStreamError,
12
+ isRetryableStreamError,
13
+ } from "./errors";
9
14
  export {
10
15
  formatSseEvent,
11
16
  proxySessionEventStream,
@@ -43,7 +48,15 @@ export type {
43
48
  StreamConnectionState,
44
49
  StreamSessionEventsOptions,
45
50
  } from "./stream";
46
- export { KNOWN_PERMISSIONS, KNOWN_USAGE_EVENT_TYPES, SESSION_EVENT_TYPES } from "./types";
51
+ export { streamWorkspaceControlEvents } from "./workspace-control-stream";
52
+ export type { WorkspaceControlStreamTransport } from "./workspace-control-stream";
53
+ export {
54
+ KNOWN_PERMISSIONS,
55
+ KNOWN_USAGE_EVENT_TYPES,
56
+ OPENGENI_API_CONTRACT_HEADER,
57
+ OPENGENI_API_CONTRACT_REVISION,
58
+ SESSION_EVENT_TYPES,
59
+ } from "./types";
47
60
  export type {
48
61
  AccessContext,
49
62
  AccessGrant,
@@ -191,6 +204,16 @@ export type {
191
204
  SessionQueueMutationResponse,
192
205
  SessionQueueSnapshot,
193
206
  SessionControlResponse,
207
+ ComposerDraft,
208
+ DeleteSessionQueueItemRequest,
209
+ EditSessionQueueItemRequest,
210
+ EffectiveControlBlocker,
211
+ EffectiveControlResumeOption,
212
+ EffectiveSessionControl,
213
+ MoveSessionQueueItemRequest,
214
+ SaveComposerDraftRequest,
215
+ SessionCommandReceipt,
216
+ SteerSessionQueueItemRequest,
194
217
  WorkspaceInferenceControlResponse,
195
218
  SessionSystemUpdate,
196
219
  SessionSystemUpdateKind,
@@ -306,6 +329,7 @@ export type {
306
329
  UserApprovalDecisionEventInput,
307
330
  UserMessageEventInput,
308
331
  Workspace,
332
+ WorkspaceControlEvent,
309
333
  VariableSet,
310
334
  VariableSetVariableMetadata,
311
335
  Rig,
package/src/stream.ts CHANGED
@@ -34,6 +34,8 @@ export type StreamSessionEventsOptions = {
34
34
  * reconnects = N+1 total open-stream calls). Defaults to unlimited.
35
35
  */
36
36
  maxReconnectAttempts?: number;
37
+ /** Await authoritative client reconciliation before exposing `live`. */
38
+ beforeLive?: (() => void | Promise<void>) | undefined;
37
39
  onStateChange?: (state: StreamConnectionState) => void;
38
40
  };
39
41
 
@@ -75,6 +77,7 @@ export async function* streamSessionEvents(
75
77
  everConnected = true;
76
78
  failedAttempts = 0;
77
79
  delayMs = baseDelayMs;
80
+ await options.beforeLive?.();
78
81
  options.onStateChange?.("live");
79
82
  for await (const message of parseSseStream(body)) {
80
83
  // Re-check after every yield resumption: an abort from the consumer