@synsci/sdk 2.0.23 → 2.0.24

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.
@@ -1,5 +1,6 @@
1
1
  export * from "./client.js";
2
2
  export * from "./server.js";
3
+ export * from "./runtime.js";
3
4
  import type { ServerOptions } from "./server.js";
4
5
  export declare function createOpenScience(options?: ServerOptions): Promise<{
5
6
  client: import("./client.js").OpenScienceClient;
@@ -1,5 +1,6 @@
1
1
  export * from "./client.js";
2
2
  export * from "./server.js";
3
+ export * from "./runtime.js";
3
4
  import { createOpenScienceClient } from "./client.js";
4
5
  import { createOpenScienceServer } from "./server.js";
5
6
  export async function createOpenScience(options) {
@@ -0,0 +1,35 @@
1
+ import { type OpenScienceClientConfig } from "./client.js";
2
+ import type { RuntimeEvent, RuntimeEventReplay, RuntimePromptAccepted, RuntimePromptData } from "./gen/types.gen.js";
3
+ export type RuntimePromptInput = NonNullable<RuntimePromptData["body"]>;
4
+ export type RuntimeEventInput = {
5
+ sessionID: string;
6
+ /** Replay events strictly after this per-session sequence. */
7
+ afterSequence?: number;
8
+ signal?: AbortSignal;
9
+ };
10
+ export declare class RuntimeEventCursorError extends Error {
11
+ readonly status = 409;
12
+ constructor(message?: string);
13
+ }
14
+ export type OpenScienceRuntimeConfig = OpenScienceClientConfig & {
15
+ directory?: string;
16
+ projectID?: string;
17
+ project?: string;
18
+ /** Delay before reconnecting an event stream that ended without an error. */
19
+ runtimeReconnectDelayMs?: number;
20
+ };
21
+ /**
22
+ * Stable, result-first facade over the generated HTTP client.
23
+ *
24
+ * The facade carries `Last-Event-ID` across reconnects; the server treats that
25
+ * ID as the authoritative cursor, so each yielded event is durably replayable
26
+ * and sequences remain monotonic within a session.
27
+ */
28
+ export declare class OpenScienceRuntime {
29
+ #private;
30
+ constructor(config?: OpenScienceRuntimeConfig);
31
+ prompt(input: RuntimePromptInput): Promise<RuntimePromptAccepted>;
32
+ replay(input: Omit<RuntimeEventInput, "signal">): Promise<RuntimeEventReplay>;
33
+ events(input: RuntimeEventInput): AsyncGenerator<RuntimeEvent, void, void>;
34
+ }
35
+ export declare function createOpenScienceRuntime(config?: OpenScienceRuntimeConfig): OpenScienceRuntime;
@@ -0,0 +1,100 @@
1
+ import { createOpenScienceClient } from "./client.js";
2
+ export class RuntimeEventCursorError extends Error {
3
+ status = 409;
4
+ constructor(message = "The runtime event cursor is outside the retained window") {
5
+ super(message);
6
+ this.name = "RuntimeEventCursorError";
7
+ }
8
+ }
9
+ function statusFromSseError(error) {
10
+ if (!(error instanceof Error))
11
+ return;
12
+ const match = error.message.match(/SSE failed:\s+(\d{3})\b/);
13
+ if (!match)
14
+ return;
15
+ return Number(match[1]);
16
+ }
17
+ function waitForReconnect(delay, signal) {
18
+ if (signal?.aborted || delay <= 0)
19
+ return Promise.resolve();
20
+ return new Promise((resolve) => {
21
+ const timer = setTimeout(done, delay);
22
+ function done() {
23
+ clearTimeout(timer);
24
+ signal?.removeEventListener("abort", done);
25
+ resolve();
26
+ }
27
+ signal?.addEventListener("abort", done, { once: true });
28
+ });
29
+ }
30
+ /**
31
+ * Stable, result-first facade over the generated HTTP client.
32
+ *
33
+ * The facade carries `Last-Event-ID` across reconnects; the server treats that
34
+ * ID as the authoritative cursor, so each yielded event is durably replayable
35
+ * and sequences remain monotonic within a session.
36
+ */
37
+ export class OpenScienceRuntime {
38
+ #client;
39
+ #reconnectDelay;
40
+ constructor(config) {
41
+ const { runtimeReconnectDelayMs: _, ...clientConfig } = config ?? {};
42
+ this.#client = createOpenScienceClient(clientConfig);
43
+ this.#reconnectDelay = Math.max(0, config?.runtimeReconnectDelayMs ?? 100);
44
+ }
45
+ prompt(input) {
46
+ return this.#client.runtime
47
+ .prompt(input, {
48
+ throwOnError: true,
49
+ })
50
+ .then((result) => result.data);
51
+ }
52
+ replay(input) {
53
+ return this.#client.runtime
54
+ .replay(input, {
55
+ throwOnError: true,
56
+ })
57
+ .then((result) => result.data);
58
+ }
59
+ async *events(input) {
60
+ const { signal, ...parameters } = input;
61
+ let lastSequence = input.afterSequence;
62
+ let failures = 0;
63
+ while (!signal?.aborted) {
64
+ let connectionError;
65
+ const result = await this.#client.runtime.subscribe(parameters, {
66
+ signal,
67
+ throwOnError: true,
68
+ // The stable facade owns reconnect policy so normal EOF and HTTP
69
+ // errors follow the same cursor-aware path.
70
+ sseMaxRetryAttempts: 1,
71
+ headers: lastSequence === undefined ? undefined : { "Last-Event-ID": String(lastSequence) },
72
+ onSseError(error) {
73
+ connectionError = error;
74
+ },
75
+ });
76
+ for await (const event of result.stream) {
77
+ lastSequence = event.sequence;
78
+ failures = 0;
79
+ yield event;
80
+ }
81
+ if (signal?.aborted)
82
+ return;
83
+ if (connectionError) {
84
+ const status = statusFromSseError(connectionError);
85
+ if (status === 409)
86
+ throw new RuntimeEventCursorError(connectionError.message);
87
+ if (status !== undefined && status >= 400 && status < 500)
88
+ throw connectionError;
89
+ failures += 1;
90
+ }
91
+ const delay = connectionError
92
+ ? Math.min(this.#reconnectDelay * 2 ** Math.max(0, failures - 1), 30_000)
93
+ : this.#reconnectDelay;
94
+ await waitForReconnect(delay, signal);
95
+ }
96
+ }
97
+ }
98
+ export function createOpenScienceRuntime(config) {
99
+ return new OpenScienceRuntime(config);
100
+ }
@@ -0,0 +1,25 @@
1
+ import type { OpenScienceClient } from "../src/v2/client.js";
2
+ import type { SettingsReviewGetData, SettingsReviewGetResponse, SettingsReviewGetResponses, SettingsReviewSetData, SettingsReviewSetResponse, SettingsReviewSetResponses } from "../src/v2/gen/types.gen.js";
3
+ type Assert<T extends true> = T;
4
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
5
+ type Field<T, K extends PropertyKey> = K extends keyof NonNullable<T> ? true : false;
6
+ type Review = OpenScienceClient["settings"]["review"];
7
+ type _reviewGet = Assert<Field<Review, "get">>;
8
+ type _reviewSet = Assert<Field<Review, "set">>;
9
+ type _getData = Assert<Equal<SettingsReviewGetData["url"], "/settings/review">>;
10
+ type _getResponses = Assert<Equal<keyof SettingsReviewGetResponses, 200>>;
11
+ type _getResponse = Assert<Equal<SettingsReviewGetResponse, SettingsReviewGetResponses[200]>>;
12
+ type _setData = Assert<Equal<SettingsReviewSetData["url"], "/settings/review">>;
13
+ type _setResponses = Assert<Equal<keyof SettingsReviewSetResponses, 200>>;
14
+ type _setResponse = Assert<Equal<SettingsReviewSetResponse, SettingsReviewSetResponses[200]>>;
15
+ export declare const reviewContract: {
16
+ readonly get: _reviewGet;
17
+ readonly set: _reviewSet;
18
+ readonly getData: _getData;
19
+ readonly getResponses: _getResponses;
20
+ readonly getResponse: _getResponse;
21
+ readonly setData: _setData;
22
+ readonly setResponses: _setResponses;
23
+ readonly setResponse: _setResponse;
24
+ };
25
+ export {};
@@ -0,0 +1,10 @@
1
+ export const reviewContract = {
2
+ get: true,
3
+ set: true,
4
+ getData: true,
5
+ getResponses: true,
6
+ getResponse: true,
7
+ setData: true,
8
+ setResponses: true,
9
+ setResponse: true,
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { createOpenScienceClient } from "../src/v2/client.js";
4
+ test("review settings retain the published get and set client contract", async () => {
5
+ const requests = [];
6
+ const client = createOpenScienceClient({
7
+ baseUrl: "http://review.test",
8
+ fetch: async (input) => {
9
+ const request = input instanceof Request ? input : new Request(input);
10
+ requests.push(request);
11
+ return Response.json({ auto: request.method === "PUT", model: null });
12
+ },
13
+ });
14
+ await client.settings.review.get();
15
+ await client.settings.review.set({ auto: true, model: null });
16
+ assert.equal(requests[0]?.url, "http://review.test/settings/review");
17
+ assert.equal(requests[0]?.method, "GET");
18
+ assert.equal(requests[1]?.url, "http://review.test/settings/review");
19
+ assert.equal(requests[1]?.method, "PUT");
20
+ assert.deepEqual(await requests[1]?.json(), { auto: true, model: null });
21
+ });
@@ -0,0 +1,19 @@
1
+ import { createOpenScienceRuntime, type RuntimePromptInput } from "../src/v2/runtime.js";
2
+ import type { RuntimeEvent, RuntimeEventReplay, RuntimePromptAccepted } from "../src/v2/gen/types.gen.js";
3
+ type Assert<T extends true> = T;
4
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
5
+ type _promptInput = Assert<Equal<RuntimePromptInput, {
6
+ sessionID: string;
7
+ message: string;
8
+ effort: "normal" | "ultra";
9
+ }>>;
10
+ type _promptResult = Assert<Equal<Awaited<ReturnType<ReturnType<typeof createOpenScienceRuntime>["prompt"]>>, RuntimePromptAccepted>>;
11
+ type _replayResult = Assert<Equal<Awaited<ReturnType<ReturnType<typeof createOpenScienceRuntime>["replay"]>>, RuntimeEventReplay>>;
12
+ type _eventsResult = Assert<Equal<ReturnType<ReturnType<typeof createOpenScienceRuntime>["events"]>, AsyncGenerator<RuntimeEvent, void, void>>>;
13
+ export declare const runtimeContract: {
14
+ promptInput: _promptInput;
15
+ promptResult: _promptResult;
16
+ replayResult: _replayResult;
17
+ eventsResult: _eventsResult;
18
+ };
19
+ export {};
@@ -0,0 +1,6 @@
1
+ export const runtimeContract = {
2
+ promptInput: true,
3
+ promptResult: true,
4
+ replayResult: true,
5
+ eventsResult: true,
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, test } from "node:test";
3
+ import { createOpenScienceRuntime, RuntimeEventCursorError } from "../src/v2/runtime.js";
4
+ describe("OpenScienceRuntime", () => {
5
+ test("sends the strict public prompt contract and returns the accepted run", async () => {
6
+ let request;
7
+ const runtime = createOpenScienceRuntime({
8
+ baseUrl: "http://runtime.test",
9
+ fetch: async (input) => {
10
+ request = input instanceof Request ? input : new Request(input);
11
+ return Response.json({ runID: "run_public", acceptedAt: 123 }, { status: 202 });
12
+ },
13
+ });
14
+ assert.deepEqual(await runtime.prompt({ sessionID: "ses_public", message: "Analyze this", effort: "ultra" }), {
15
+ runID: "run_public",
16
+ acceptedAt: 123,
17
+ });
18
+ assert.equal(request?.url, "http://runtime.test/runtime/prompt");
19
+ assert.equal(request?.method, "POST");
20
+ assert.deepEqual(await request?.json(), {
21
+ sessionID: "ses_public",
22
+ message: "Analyze this",
23
+ effort: "ultra",
24
+ });
25
+ });
26
+ test("generated prompt serialization keeps normal effort in the request body", async () => {
27
+ let body;
28
+ const runtime = createOpenScienceRuntime({
29
+ baseUrl: "http://runtime.test",
30
+ fetch: async (input) => {
31
+ const request = input instanceof Request ? input : new Request(input);
32
+ body = await request.json();
33
+ return Response.json({ runID: "run_normal", acceptedAt: 321 }, { status: 202 });
34
+ },
35
+ });
36
+ await runtime.prompt({ sessionID: "ses_normal", message: "Summarize", effort: "normal" });
37
+ assert.deepEqual(body, { sessionID: "ses_normal", message: "Summarize", effort: "normal" });
38
+ });
39
+ test("replays from a sequence cursor and yields typed SSE events", async () => {
40
+ const urls = [];
41
+ const controller = new AbortController();
42
+ const event = {
43
+ sequence: 4,
44
+ sessionID: "ses_public",
45
+ runID: "run_public",
46
+ type: "runtime.completed",
47
+ properties: { messageID: "msg_public" },
48
+ time: 456,
49
+ };
50
+ const runtime = createOpenScienceRuntime({
51
+ baseUrl: "http://runtime.test",
52
+ fetch: async (input) => {
53
+ const request = input instanceof Request ? input : new Request(input);
54
+ urls.push(request.url);
55
+ if (request.url.includes("/replay")) {
56
+ return Response.json({ events: [event], oldestSequence: 1, latestSequence: 4 });
57
+ }
58
+ return new Response(`id: 4\nevent: runtime.completed\ndata: ${JSON.stringify(event)}\n\n`, {
59
+ headers: { "content-type": "text/event-stream" },
60
+ });
61
+ },
62
+ });
63
+ assert.deepEqual(await runtime.replay({ sessionID: "ses_public", afterSequence: 3 }), {
64
+ events: [event],
65
+ oldestSequence: 1,
66
+ latestSequence: 4,
67
+ });
68
+ const received = [];
69
+ for await (const item of runtime.events({ sessionID: "ses_public", afterSequence: 3, signal: controller.signal })) {
70
+ received.push(item);
71
+ controller.abort();
72
+ }
73
+ assert.deepEqual(received, [event]);
74
+ assert.deepEqual(urls, [
75
+ "http://runtime.test/runtime/events/replay?sessionID=ses_public&afterSequence=3",
76
+ "http://runtime.test/runtime/events?sessionID=ses_public&afterSequence=3",
77
+ ]);
78
+ });
79
+ test("reconnects after clean EOF with the last delivered sequence", async () => {
80
+ const requests = [];
81
+ const controller = new AbortController();
82
+ const event = (sequence) => ({
83
+ sequence,
84
+ sessionID: "ses_reconnect",
85
+ runID: "run_reconnect",
86
+ type: sequence === 1 ? "runtime.accepted" : "runtime.completed",
87
+ properties: sequence === 1 ? { effort: "normal" } : { messageID: "msg_done" },
88
+ time: sequence,
89
+ });
90
+ const runtime = createOpenScienceRuntime({
91
+ baseUrl: "http://runtime.test",
92
+ runtimeReconnectDelayMs: 0,
93
+ fetch: async (input) => {
94
+ const request = input instanceof Request ? input : new Request(input);
95
+ requests.push(request);
96
+ const current = event(requests.length);
97
+ return new Response(`id: ${current.sequence}\nevent: ${current.type}\ndata: ${JSON.stringify(current)}\n\n`, {
98
+ headers: { "content-type": "text/event-stream" },
99
+ });
100
+ },
101
+ });
102
+ const received = [];
103
+ for await (const item of runtime.events({
104
+ sessionID: "ses_reconnect",
105
+ afterSequence: 0,
106
+ signal: controller.signal,
107
+ })) {
108
+ received.push(item);
109
+ if (received.length === 2)
110
+ controller.abort();
111
+ }
112
+ assert.deepEqual(received.map((item) => item.sequence), [1, 2]);
113
+ assert.equal(requests.length, 2);
114
+ assert.equal(requests[0]?.headers.get("Last-Event-ID"), "0");
115
+ assert.equal(requests[1]?.headers.get("Last-Event-ID"), "1");
116
+ });
117
+ test("surfaces a retained-window conflict without retrying forever", async () => {
118
+ let calls = 0;
119
+ const runtime = createOpenScienceRuntime({
120
+ baseUrl: "http://runtime.test",
121
+ runtimeReconnectDelayMs: 0,
122
+ fetch: async () => {
123
+ calls += 1;
124
+ return Response.json({ error: "cursor_expired" }, { status: 409, statusText: "Conflict" });
125
+ },
126
+ });
127
+ await assert.rejects(async () => {
128
+ for await (const _ of runtime.events({ sessionID: "ses_expired", afterSequence: 1 })) {
129
+ // No event is expected.
130
+ }
131
+ }, RuntimeEventCursorError);
132
+ assert.equal(calls, 1);
133
+ });
134
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@synsci/sdk",
4
- "version": "2.0.23",
4
+ "version": "2.0.24",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "scripts": {