@cubos/agent-sdk 0.0.1142885 → 0.0.1142903
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/README.md +22 -7
- package/dist/client.d.ts +18 -3
- package/dist/errors.d.ts +7 -0
- package/dist/generated/schema.d.ts +10 -0
- package/dist/index.js +156 -56
- package/dist/index.js.map +6 -6
- package/dist/sse.d.ts +21 -3
- package/dist/sse.js +23 -6
- package/dist/sse.js.map +4 -4
- package/package.json +1 -1
package/dist/sse.d.ts
CHANGED
|
@@ -32,6 +32,20 @@ export interface SseOptions<T> {
|
|
|
32
32
|
/** Resume cursor for the first connect. Later reconnects use the last `id:`
|
|
33
33
|
* the server actually sent. */
|
|
34
34
|
lastEventId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The cursor as a shared cell instead: read before every connect and written
|
|
37
|
+
* after every frame, so something else delivering the same log — a poll that
|
|
38
|
+
* runs while a connection is unproven — can advance it, and the next connect
|
|
39
|
+
* asks for what neither has seen rather than replaying what the other did.
|
|
40
|
+
* Takes precedence over `lastEventId`.
|
|
41
|
+
*/
|
|
42
|
+
cursor?: {
|
|
43
|
+
lastEventId?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Every time a connection ends, for whatever reason, before the wait that
|
|
46
|
+
* precedes the next attempt. The counterpart of `onOpen`: between the two a
|
|
47
|
+
* connection was live, and outside them nothing is being received. */
|
|
48
|
+
onClose?: () => void;
|
|
35
49
|
fetchImpl?: FetchLike;
|
|
36
50
|
/**
|
|
37
51
|
* Reconnect when this many milliseconds pass with no bytes at all — not even
|
|
@@ -64,10 +78,14 @@ export declare class SseIdleTimeout extends Error {
|
|
|
64
78
|
*
|
|
65
79
|
* `Last-Event-ID` is replayed from the last frame the server sent, so the
|
|
66
80
|
* backend's catch-up query fills whatever the gap swallowed. 4xx ends the loop
|
|
67
|
-
* — a deleted conversation won't fix itself by retrying — with
|
|
68
|
-
* 401 buys one immediate reconnect with `forceRefresh`, because a short-lived
|
|
81
|
+
* — a deleted conversation won't fix itself by retrying — with two exceptions.
|
|
82
|
+
* A 401 buys one immediate reconnect with `forceRefresh`, because a short-lived
|
|
69
83
|
* token expiring under a stream that outlives it is the expected case, not a
|
|
70
|
-
* misconfiguration
|
|
84
|
+
* misconfiguration; only after that second rejection does the promise reject.
|
|
85
|
+
* And a 429 is a refusal, not a verdict: the user's budget or their cap on open
|
|
86
|
+
* streams is full, and the server says when to come back — so the loop waits
|
|
87
|
+
* out `Retry-After` and tries again, rather than ending a subscription over a
|
|
88
|
+
* limit that clears in seconds.
|
|
71
89
|
*
|
|
72
90
|
* A connection that goes silent past `idleTimeoutMs` is dropped and reopened:
|
|
73
91
|
* see that option for why waiting on `read()` forever is not an option.
|
package/dist/sse.js
CHANGED
|
@@ -76,6 +76,17 @@ async function raiseForStatus(res, fallback) {
|
|
|
76
76
|
const base = DEFAULT_MESSAGES[res.status] ?? fallback;
|
|
77
77
|
throw new AgentApiError(detail ? `${base} (${detail.slice(0, 500)})` : base, res.status, res.headers.get("x-request-id"), detail);
|
|
78
78
|
}
|
|
79
|
+
function parseRetryAfter(header) {
|
|
80
|
+
if (!header)
|
|
81
|
+
return null;
|
|
82
|
+
const seconds = Number(header.trim());
|
|
83
|
+
if (Number.isFinite(seconds))
|
|
84
|
+
return Math.max(0, seconds * 1000);
|
|
85
|
+
const date = Date.parse(header);
|
|
86
|
+
if (Number.isNaN(date))
|
|
87
|
+
return null;
|
|
88
|
+
return Math.max(0, date - Date.now());
|
|
89
|
+
}
|
|
79
90
|
|
|
80
91
|
// src/sse.ts
|
|
81
92
|
var MAX_BACKOFF_MS = 30000;
|
|
@@ -98,11 +109,12 @@ class SseIdleTimeout extends Error {
|
|
|
98
109
|
async function readSse(opts) {
|
|
99
110
|
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
100
111
|
const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
101
|
-
|
|
112
|
+
const cursor = opts.cursor ?? { lastEventId: opts.lastEventId };
|
|
102
113
|
let attempt = 0;
|
|
103
114
|
let refreshing = false;
|
|
104
115
|
while (!opts.signal.aborted) {
|
|
105
116
|
let madeProgress = false;
|
|
117
|
+
let retryAfterMs = null;
|
|
106
118
|
const connection = new AbortController;
|
|
107
119
|
const unlink = forward(opts.signal, connection);
|
|
108
120
|
let idleTimer;
|
|
@@ -121,8 +133,8 @@ async function readSse(opts) {
|
|
|
121
133
|
...await opts.headers?.({ forceRefresh: refreshing }),
|
|
122
134
|
Accept: "text/event-stream"
|
|
123
135
|
};
|
|
124
|
-
if (lastEventId !== undefined)
|
|
125
|
-
headers["Last-Event-ID"] = lastEventId;
|
|
136
|
+
if (cursor.lastEventId !== undefined)
|
|
137
|
+
headers["Last-Event-ID"] = cursor.lastEventId;
|
|
126
138
|
armIdle();
|
|
127
139
|
const res = await doFetch(opts.url, { headers, signal: connection.signal });
|
|
128
140
|
if (opts.signal.aborted)
|
|
@@ -136,6 +148,10 @@ async function readSse(opts) {
|
|
|
136
148
|
refreshing = true;
|
|
137
149
|
continue;
|
|
138
150
|
}
|
|
151
|
+
if (res.status === 429) {
|
|
152
|
+
retryAfterMs = Math.min(MAX_BACKOFF_MS, parseRetryAfter(res.headers.get("retry-after")) ?? MAX_BACKOFF_MS);
|
|
153
|
+
await raiseForStatus(res, `Could not open ${opts.url}.`);
|
|
154
|
+
}
|
|
139
155
|
if (res.status >= 400 && res.status < 500) {
|
|
140
156
|
await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {
|
|
141
157
|
throw new Fatal(err);
|
|
@@ -149,7 +165,7 @@ async function readSse(opts) {
|
|
|
149
165
|
if (parsed === null)
|
|
150
166
|
continue;
|
|
151
167
|
if (parsed.id !== null)
|
|
152
|
-
lastEventId = parsed.id;
|
|
168
|
+
cursor.lastEventId = parsed.id;
|
|
153
169
|
opts.onEvent(parsed.data, parsed.event);
|
|
154
170
|
madeProgress = true;
|
|
155
171
|
}
|
|
@@ -165,12 +181,13 @@ async function readSse(opts) {
|
|
|
165
181
|
clearTimeout(idleTimer);
|
|
166
182
|
unlink();
|
|
167
183
|
connection.abort();
|
|
184
|
+
opts.onClose?.();
|
|
168
185
|
}
|
|
169
186
|
if (opts.signal.aborted)
|
|
170
187
|
return;
|
|
171
188
|
if (madeProgress)
|
|
172
189
|
attempt = 0;
|
|
173
|
-
const delayMs = Math.min(MAX_BACKOFF_MS, 1000 * 2 ** attempt);
|
|
190
|
+
const delayMs = retryAfterMs ?? Math.min(MAX_BACKOFF_MS, 1000 * 2 ** attempt);
|
|
174
191
|
attempt += 1;
|
|
175
192
|
await sleep(delayMs, opts.signal);
|
|
176
193
|
}
|
|
@@ -252,5 +269,5 @@ export {
|
|
|
252
269
|
DEFAULT_IDLE_TIMEOUT_MS
|
|
253
270
|
};
|
|
254
271
|
|
|
255
|
-
//# debugId=
|
|
272
|
+
//# debugId=3ACA84B6D428D3C464756E2164756E21
|
|
256
273
|
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/errors.ts", "../src/sse.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/** Base for everything this SDK throws, so `catch (e) { if (e instanceof\n * AgentError) }` covers both an HTTP failure and a connection that never got\n * there. */\nexport class AgentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AgentError\";\n }\n}\n\n/** The server answered, and said no. */\nexport class AgentApiError extends AgentError {\n readonly status: number;\n /** Server's `x-request-id`, when present. Worth quoting in a bug report. */\n readonly requestId: string | null;\n /** The response body, verbatim and untruncated. Some routes explain the\n * failure in there (an MCP probe's reason, a rejected cron) in a form worth\n * showing the user; `message` only carries a truncated preview. */\n readonly body: string;\n\n constructor(message: string, status: number, requestId: string | null = null, body = \"\") {\n super(message);\n this.name = \"AgentApiError\";\n this.status = status;\n this.requestId = requestId;\n this.body = body;\n }\n\n /** The body parsed as JSON, or `undefined` when it isn't. */\n json<T = unknown>(): T | undefined {\n try {\n return JSON.parse(this.body) as T;\n } catch {\n return undefined;\n }\n }\n\n /** The token was rejected. The client already retried once with a fresh one,\n * so seeing this means `getToken` is handing back something unusable. */\n get isAuthError(): boolean {\n return this.status === 401 || this.status === 403;\n }\n\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** The request clashed with current state — a slug already taken, a\n * conversation that belongs to a channel, a user already blocked. */\n get isConflict(): boolean {\n return this.status === 409;\n }\n\n /** Worth retrying after a pause: the server is overloaded or briefly down. */\n get isRetryable(): boolean {\n return this.status === 429 || this.status >= 500;\n }\n}\n\n/**\n * The client was constructed wrong — a `baseUrl` with no scheme, most often.\n * Thrown at construction, not on the first call, so the stack points at the\n * mistake.\n */\nexport class AgentConfigError extends AgentError {\n constructor(message: string) {\n super(message);\n this.name = \"AgentConfigError\";\n }\n}\n\n/**\n * The request never produced a response: DNS, TLS, a refused connection, CORS,\n * or the timeout below.\n *\n * Without this, `fetch` rejects with a bare `TypeError: fetch failed` and the\n * caller cannot tell a wrong `baseUrl` from a server that said 500 — the two\n * need completely different fixes.\n */\nexport class AgentNetworkError extends AgentError {\n /** Whatever `fetch` (or the abort) threw. */\n readonly cause: unknown;\n /** True when the SDK's own timeout fired rather than the network failing. */\n readonly timedOut: boolean;\n\n constructor(message: string, cause: unknown, timedOut = false) {\n super(message);\n this.name = \"AgentNetworkError\";\n this.cause = cause;\n this.timedOut = timedOut;\n }\n}\n\nconst DEFAULT_MESSAGES: Record<number, string> = {\n 400: \"Invalid request.\",\n 401: \"Not authenticated.\",\n 403: \"Not allowed.\",\n 404: \"Not found.\",\n 409: \"Conflicts with the current state.\",\n 413: \"Payload too large.\",\n 429: \"Rate limited.\",\n};\n\nexport async function raiseForStatus(res: Response, fallback: string): Promise<void> {\n if (res.ok) return;\n let detail = \"\";\n try {\n detail = await res.text();\n } catch {\n detail = \"\";\n }\n const base = DEFAULT_MESSAGES[res.status] ?? fallback;\n throw new AgentApiError(\n detail ? `${base} (${detail.slice(0, 500)})` : base,\n res.status,\n res.headers.get(\"x-request-id\"),\n detail,\n );\n}\n",
|
|
6
|
-
"// A `fetch`-based Server-Sent Events reader. Not `EventSource`: that can't\n// attach an Authorization header, which every stream here requires.\n//\n// Runtime-agnostic on purpose — `fetch`, `ReadableStream` and `AbortController`\n// only, no DOM. Exported as its own entry point (`@cubos/agent-sdk/sse`) so the\n// operator dashboard can reuse it without adopting the rest of the client.\n\nimport { raiseForStatus } from \"./errors.js\";\n\n/** Just the call signature, not `typeof fetch` — that also demands runtime\n * extras (Bun's `preconnect`, undici's statics) a caller's wrapper won't have. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string | FormData;\n signal?: AbortSignal;\n },\n) => Promise<Response>;\n\nexport interface SseOptions<T> {\n url: string;\n /**\n * Only frames with one of these `event:` names are delivered; the rest are\n * dropped. Several names on one connection is how a stream keeps two kinds of\n * frame in a single order — split across two connections there is none, and a\n * client cannot tell which came first.\n */\n event: string | string[];\n onEvent: (data: T, event: string) => void;\n signal: AbortSignal;\n /** Called before each (re)connect, so the caller can mint a fresh token.\n * `forceRefresh` is set on the one immediate retry that follows a 401 — a\n * caller that caches a token must bypass the cache when it sees it, or the\n * retry replays the rejected credential and the stream dies for good. */\n headers?: (opts: {\n forceRefresh: boolean;\n }) => Promise<Record<string, string>> | Record<string, string>;\n /** Called once per successful connect, before any frame is delivered. The\n * hook a caller needs to catch up on state the stream won't replay: it fires\n * on every reconnect too, so a gap the backoff swallowed is covered as\n * well. Not awaited — a slow catch-up must not stall frame delivery. */\n onOpen?: () => void;\n /** Resume cursor for the first connect. Later reconnects use the last `id:`\n * the server actually sent. */\n lastEventId?: string;\n fetchImpl?: FetchLike;\n /**\n * Reconnect when this many milliseconds pass with no bytes at all — not even\n * a keep-alive comment. Defaults to `DEFAULT_IDLE_TIMEOUT_MS`; `0` disables.\n *\n * A half-open socket (a phone leaving the background, a proxy that dropped\n * the connection without telling either end) leaves `read()` waiting forever,\n * with nothing for the reconnect loop below to react to. The server sends a\n * comment every 15s, so silence past twice that is the connection being gone\n * rather than the agent being quiet.\n */\n idleTimeoutMs?: number;\n /** Every failed attempt, transient or fatal. The loop is already reconnecting\n * when this fires unless the promise also rejects. */\n onError?: (err: unknown) => void;\n}\n\nconst MAX_BACKOFF_MS = 30_000;\n\n/** Twice the server's 15s keep-alive interval: one comment may be lost to a\n * hiccup without the connection being gone, two in a row means it is. */\nexport const DEFAULT_IDLE_TIMEOUT_MS = 30_000;\n\n/** Marks an error the reconnect loop must not swallow. */\nclass Fatal extends Error {\n override readonly cause: unknown;\n constructor(cause: unknown) {\n super(\"fatal stream error\");\n this.cause = cause;\n }\n}\n\n/** Raised when the watchdog trips, so a caller logging `onError` can tell a\n * connection that went silent from one that failed outright. */\nexport class SseIdleTimeout extends Error {\n constructor(url: string, ms: number) {\n super(`Stream ${url} sent nothing for ${ms}ms — reconnecting.`);\n this.name = \"SseIdleTimeout\";\n }\n}\n\n/**\n * Reads `url` until `signal` aborts, reconnecting through any drop — clean\n * close from the server included, since intermediaries (Cloudflare et al.)\n * close idle SSE connections without warning and the caller would otherwise\n * silently stop receiving.\n *\n * `Last-Event-ID` is replayed from the last frame the server sent, so the\n * backend's catch-up query fills whatever the gap swallowed. 4xx ends the loop\n * — a deleted conversation won't fix itself by retrying — with one exception: a\n * 401 buys one immediate reconnect with `forceRefresh`, because a short-lived\n * token expiring under a stream that outlives it is the expected case, not a\n * misconfiguration. Only after that second rejection does the promise reject.\n *\n * A connection that goes silent past `idleTimeoutMs` is dropped and reopened:\n * see that option for why waiting on `read()` forever is not an option.\n */\nexport async function readSse<T>(opts: SseOptions<T>): Promise<void> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n let lastEventId = opts.lastEventId;\n let attempt = 0;\n // Set by a 401 and consumed by the connect that follows it, so the retry is\n // the only attempt that pays for a freshly minted token.\n let refreshing = false;\n\n while (!opts.signal.aborted) {\n let madeProgress = false;\n // Aborts this connection alone — the watchdog must be able to drop a dead\n // socket without ending the subscription the caller's signal owns.\n const connection = new AbortController();\n const unlink = forward(opts.signal, connection);\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n let wentIdle = false;\n const armIdle = () => {\n if (idleMs <= 0) return;\n clearTimeout(idleTimer);\n idleTimer = setTimeout(() => {\n wentIdle = true;\n connection.abort();\n }, idleMs);\n };\n\n try {\n const headers: Record<string, string> = {\n ...(await opts.headers?.({ forceRefresh: refreshing })),\n Accept: \"text/event-stream\",\n };\n if (lastEventId !== undefined) headers[\"Last-Event-ID\"] = lastEventId;\n\n // Armed before the fetch: a connect that never answers is as dead as one\n // that stops mid-stream, and nothing else bounds it.\n armIdle();\n const res = await doFetch(opts.url, { headers, signal: connection.signal });\n if (opts.signal.aborted) return;\n if (res.ok && res.body) {\n refreshing = false;\n opts.onOpen?.();\n }\n\n if (!res.ok || !res.body) {\n // One immediate retry, no backoff: a token that expired between minting\n // and this connect is the expected case for a stream that outlives its\n // TTL, and `headers` is the only thing that can produce a new one. A\n // credential that is simply wrong takes the fatal path on the retry.\n if (res.status === 401 && opts.headers && !refreshing) {\n refreshing = true;\n continue;\n }\n if (res.status >= 400 && res.status < 500) {\n // Wrapped so the catch below can tell it apart from a transient\n // failure and rethrow instead of reconnecting forever.\n await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {\n throw new Fatal(err);\n });\n return;\n }\n throw new Error(`stream open failed with HTTP ${res.status}`);\n }\n\n for await (const frame of frames(res.body, connection.signal, armIdle)) {\n const parsed = parseFrame<T>(frame, opts.event);\n if (parsed === null) continue;\n if (parsed.id !== null) lastEventId = parsed.id;\n opts.onEvent(parsed.data, parsed.event);\n madeProgress = true;\n }\n if (wentIdle) throw new SseIdleTimeout(opts.url, idleMs);\n } catch (err) {\n if (opts.signal.aborted) return;\n // A deleted conversation, or a credential a refresh didn't fix.\n if (err instanceof Fatal) throw err.cause;\n // Everything else is transient (network, 5xx, proxy hangup, a socket that\n // went silent) — report and back off rather than end the subscription.\n opts.onError?.(wentIdle ? new SseIdleTimeout(opts.url, idleMs) : err);\n } finally {\n clearTimeout(idleTimer);\n unlink();\n // Nothing below this point reads from the body; letting it hang open\n // would leak a connection per reconnect.\n connection.abort();\n }\n\n if (opts.signal.aborted) return;\n\n // Any delivered frame resets the backoff: a long-lived stream that did real\n // work and then dropped should come back fast, not wait out the ceiling.\n if (madeProgress) attempt = 0;\n const delayMs = Math.min(MAX_BACKOFF_MS, 1_000 * 2 ** attempt);\n attempt += 1;\n await sleep(delayMs, opts.signal);\n }\n}\n\n/** `onRead` fires for every chunk the socket delivers, keep-alive comments\n * included — the watchdog is asking whether the connection is alive, which is\n * not the same question as whether a frame the caller wanted came through. */\nasync function* frames(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n onRead: () => void,\n): AsyncGenerator<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n while (!signal.aborted) {\n const { value, done } = await reader.read();\n if (done) return;\n onRead();\n buffer += decoder.decode(value, { stream: true });\n for (;;) {\n const sep = buffer.indexOf(\"\\n\\n\");\n if (sep === -1) break;\n yield buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n }\n }\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n\nexport function parseFrame<T>(\n frame: string,\n expectedEvent: string | string[],\n): { data: T; id: string | null; event: string } | null {\n let dataLine: string | null = null;\n let id: string | null = null;\n let eventName = \"message\";\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\":\")) continue;\n if (line.startsWith(\"data:\")) dataLine = line.slice(5).trimStart();\n else if (line.startsWith(\"event:\")) eventName = line.slice(6).trim();\n else if (line.startsWith(\"id:\")) id = line.slice(3).trim();\n }\n const wanted =\n typeof expectedEvent === \"string\"\n ? eventName === expectedEvent\n : expectedEvent.includes(eventName);\n if (!wanted || dataLine === null) return null;\n try {\n return { data: JSON.parse(dataLine) as T, id, event: eventName };\n } catch {\n return null;\n }\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** Aborts `child` when `outer` does, and hands back the unsubscribe — without\n * it every reconnect would leave another listener on a signal that lives as\n * long as the subscription. */\nfunction forward(outer: AbortSignal, child: AbortController): () => void {\n const onAbort = () => child.abort();\n if (outer.aborted) child.abort();\n else outer.addEventListener(\"abort\", onAbort, { once: true });\n return () => outer.removeEventListener(\"abort\", onAbort);\n}\n"
|
|
5
|
+
"/** Base for everything this SDK throws, so `catch (e) { if (e instanceof\n * AgentError) }` covers both an HTTP failure and a connection that never got\n * there. */\nexport class AgentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AgentError\";\n }\n}\n\n/** The server answered, and said no. */\nexport class AgentApiError extends AgentError {\n readonly status: number;\n /** Server's `x-request-id`, when present. Worth quoting in a bug report. */\n readonly requestId: string | null;\n /** The response body, verbatim and untruncated. Some routes explain the\n * failure in there (an MCP probe's reason, a rejected cron) in a form worth\n * showing the user; `message` only carries a truncated preview. */\n readonly body: string;\n\n constructor(message: string, status: number, requestId: string | null = null, body = \"\") {\n super(message);\n this.name = \"AgentApiError\";\n this.status = status;\n this.requestId = requestId;\n this.body = body;\n }\n\n /** The body parsed as JSON, or `undefined` when it isn't. */\n json<T = unknown>(): T | undefined {\n try {\n return JSON.parse(this.body) as T;\n } catch {\n return undefined;\n }\n }\n\n /** The token was rejected. The client already retried once with a fresh one,\n * so seeing this means `getToken` is handing back something unusable. */\n get isAuthError(): boolean {\n return this.status === 401 || this.status === 403;\n }\n\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** The request clashed with current state — a slug already taken, a\n * conversation that belongs to a channel, a user already blocked. */\n get isConflict(): boolean {\n return this.status === 409;\n }\n\n /** Worth retrying after a pause: the server is overloaded or briefly down. */\n get isRetryable(): boolean {\n return this.status === 429 || this.status >= 500;\n }\n}\n\n/**\n * The client was constructed wrong — a `baseUrl` with no scheme, most often.\n * Thrown at construction, not on the first call, so the stack points at the\n * mistake.\n */\nexport class AgentConfigError extends AgentError {\n constructor(message: string) {\n super(message);\n this.name = \"AgentConfigError\";\n }\n}\n\n/**\n * The request never produced a response: DNS, TLS, a refused connection, CORS,\n * or the timeout below.\n *\n * Without this, `fetch` rejects with a bare `TypeError: fetch failed` and the\n * caller cannot tell a wrong `baseUrl` from a server that said 500 — the two\n * need completely different fixes.\n */\nexport class AgentNetworkError extends AgentError {\n /** Whatever `fetch` (or the abort) threw. */\n readonly cause: unknown;\n /** True when the SDK's own timeout fired rather than the network failing. */\n readonly timedOut: boolean;\n\n constructor(message: string, cause: unknown, timedOut = false) {\n super(message);\n this.name = \"AgentNetworkError\";\n this.cause = cause;\n this.timedOut = timedOut;\n }\n}\n\nconst DEFAULT_MESSAGES: Record<number, string> = {\n 400: \"Invalid request.\",\n 401: \"Not authenticated.\",\n 403: \"Not allowed.\",\n 404: \"Not found.\",\n 409: \"Conflicts with the current state.\",\n 413: \"Payload too large.\",\n 429: \"Rate limited.\",\n};\n\nexport async function raiseForStatus(res: Response, fallback: string): Promise<void> {\n if (res.ok) return;\n let detail = \"\";\n try {\n detail = await res.text();\n } catch {\n detail = \"\";\n }\n const base = DEFAULT_MESSAGES[res.status] ?? fallback;\n throw new AgentApiError(\n detail ? `${base} (${detail.slice(0, 500)})` : base,\n res.status,\n res.headers.get(\"x-request-id\"),\n detail,\n );\n}\n\n/**\n * Milliseconds from a `Retry-After`, which is either a delta in seconds or an\n * HTTP date. Returns null when absent or unusable, so the caller can decide not\n * to retry rather than guess an interval. Shared by the request retry and the\n * stream reconnect, which honour the same header from the same limiter.\n */\nexport function parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const seconds = Number(header.trim());\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const date = Date.parse(header);\n if (Number.isNaN(date)) return null;\n return Math.max(0, date - Date.now());\n}\n",
|
|
6
|
+
"// A `fetch`-based Server-Sent Events reader. Not `EventSource`: that can't\n// attach an Authorization header, which every stream here requires.\n//\n// Runtime-agnostic on purpose — `fetch`, `ReadableStream` and `AbortController`\n// only, no DOM. Exported as its own entry point (`@cubos/agent-sdk/sse`) so the\n// operator dashboard can reuse it without adopting the rest of the client.\n\nimport { parseRetryAfter, raiseForStatus } from \"./errors.js\";\n\n/** Just the call signature, not `typeof fetch` — that also demands runtime\n * extras (Bun's `preconnect`, undici's statics) a caller's wrapper won't have. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string | FormData;\n signal?: AbortSignal;\n },\n) => Promise<Response>;\n\nexport interface SseOptions<T> {\n url: string;\n /**\n * Only frames with one of these `event:` names are delivered; the rest are\n * dropped. Several names on one connection is how a stream keeps two kinds of\n * frame in a single order — split across two connections there is none, and a\n * client cannot tell which came first.\n */\n event: string | string[];\n onEvent: (data: T, event: string) => void;\n signal: AbortSignal;\n /** Called before each (re)connect, so the caller can mint a fresh token.\n * `forceRefresh` is set on the one immediate retry that follows a 401 — a\n * caller that caches a token must bypass the cache when it sees it, or the\n * retry replays the rejected credential and the stream dies for good. */\n headers?: (opts: {\n forceRefresh: boolean;\n }) => Promise<Record<string, string>> | Record<string, string>;\n /** Called once per successful connect, before any frame is delivered. The\n * hook a caller needs to catch up on state the stream won't replay: it fires\n * on every reconnect too, so a gap the backoff swallowed is covered as\n * well. Not awaited — a slow catch-up must not stall frame delivery. */\n onOpen?: () => void;\n /** Resume cursor for the first connect. Later reconnects use the last `id:`\n * the server actually sent. */\n lastEventId?: string;\n /**\n * The cursor as a shared cell instead: read before every connect and written\n * after every frame, so something else delivering the same log — a poll that\n * runs while a connection is unproven — can advance it, and the next connect\n * asks for what neither has seen rather than replaying what the other did.\n * Takes precedence over `lastEventId`.\n */\n cursor?: { lastEventId?: string };\n /** Every time a connection ends, for whatever reason, before the wait that\n * precedes the next attempt. The counterpart of `onOpen`: between the two a\n * connection was live, and outside them nothing is being received. */\n onClose?: () => void;\n fetchImpl?: FetchLike;\n /**\n * Reconnect when this many milliseconds pass with no bytes at all — not even\n * a keep-alive comment. Defaults to `DEFAULT_IDLE_TIMEOUT_MS`; `0` disables.\n *\n * A half-open socket (a phone leaving the background, a proxy that dropped\n * the connection without telling either end) leaves `read()` waiting forever,\n * with nothing for the reconnect loop below to react to. The server sends a\n * comment every 15s, so silence past twice that is the connection being gone\n * rather than the agent being quiet.\n */\n idleTimeoutMs?: number;\n /** Every failed attempt, transient or fatal. The loop is already reconnecting\n * when this fires unless the promise also rejects. */\n onError?: (err: unknown) => void;\n}\n\nconst MAX_BACKOFF_MS = 30_000;\n\n/** Twice the server's 15s keep-alive interval: one comment may be lost to a\n * hiccup without the connection being gone, two in a row means it is. */\nexport const DEFAULT_IDLE_TIMEOUT_MS = 30_000;\n\n/** Marks an error the reconnect loop must not swallow. */\nclass Fatal extends Error {\n override readonly cause: unknown;\n constructor(cause: unknown) {\n super(\"fatal stream error\");\n this.cause = cause;\n }\n}\n\n/** Raised when the watchdog trips, so a caller logging `onError` can tell a\n * connection that went silent from one that failed outright. */\nexport class SseIdleTimeout extends Error {\n constructor(url: string, ms: number) {\n super(`Stream ${url} sent nothing for ${ms}ms — reconnecting.`);\n this.name = \"SseIdleTimeout\";\n }\n}\n\n/**\n * Reads `url` until `signal` aborts, reconnecting through any drop — clean\n * close from the server included, since intermediaries (Cloudflare et al.)\n * close idle SSE connections without warning and the caller would otherwise\n * silently stop receiving.\n *\n * `Last-Event-ID` is replayed from the last frame the server sent, so the\n * backend's catch-up query fills whatever the gap swallowed. 4xx ends the loop\n * — a deleted conversation won't fix itself by retrying — with two exceptions.\n * A 401 buys one immediate reconnect with `forceRefresh`, because a short-lived\n * token expiring under a stream that outlives it is the expected case, not a\n * misconfiguration; only after that second rejection does the promise reject.\n * And a 429 is a refusal, not a verdict: the user's budget or their cap on open\n * streams is full, and the server says when to come back — so the loop waits\n * out `Retry-After` and tries again, rather than ending a subscription over a\n * limit that clears in seconds.\n *\n * A connection that goes silent past `idleTimeoutMs` is dropped and reopened:\n * see that option for why waiting on `read()` forever is not an option.\n */\nexport async function readSse<T>(opts: SseOptions<T>): Promise<void> {\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n const cursor = opts.cursor ?? { lastEventId: opts.lastEventId };\n let attempt = 0;\n // Set by a 401 and consumed by the connect that follows it, so the retry is\n // the only attempt that pays for a freshly minted token.\n let refreshing = false;\n\n while (!opts.signal.aborted) {\n let madeProgress = false;\n // Set by a 429 from the server's own `Retry-After`; overrides the\n // exponential backoff for the wait that follows.\n let retryAfterMs: number | null = null;\n // Aborts this connection alone — the watchdog must be able to drop a dead\n // socket without ending the subscription the caller's signal owns.\n const connection = new AbortController();\n const unlink = forward(opts.signal, connection);\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n let wentIdle = false;\n const armIdle = () => {\n if (idleMs <= 0) return;\n clearTimeout(idleTimer);\n idleTimer = setTimeout(() => {\n wentIdle = true;\n connection.abort();\n }, idleMs);\n };\n\n try {\n const headers: Record<string, string> = {\n ...(await opts.headers?.({ forceRefresh: refreshing })),\n Accept: \"text/event-stream\",\n };\n if (cursor.lastEventId !== undefined) headers[\"Last-Event-ID\"] = cursor.lastEventId;\n\n // Armed before the fetch: a connect that never answers is as dead as one\n // that stops mid-stream, and nothing else bounds it.\n armIdle();\n const res = await doFetch(opts.url, { headers, signal: connection.signal });\n if (opts.signal.aborted) return;\n if (res.ok && res.body) {\n refreshing = false;\n opts.onOpen?.();\n }\n\n if (!res.ok || !res.body) {\n // One immediate retry, no backoff: a token that expired between minting\n // and this connect is the expected case for a stream that outlives its\n // TTL, and `headers` is the only thing that can produce a new one. A\n // credential that is simply wrong takes the fatal path on the retry.\n if (res.status === 401 && opts.headers && !refreshing) {\n refreshing = true;\n continue;\n }\n if (res.status === 429) {\n // Transient by definition, and the one 4xx an app can hit by\n // doing nothing wrong: a user with one tab too many is over the\n // per-user cap on open streams, and each tab's stream would die\n // for good here while the cap clears the moment any tab closes.\n // Without a usable header the ceiling is the wait — the limiter\n // always sends one, so that is the odd proxy, not the server.\n retryAfterMs = Math.min(\n MAX_BACKOFF_MS,\n parseRetryAfter(res.headers.get(\"retry-after\")) ?? MAX_BACKOFF_MS,\n );\n await raiseForStatus(res, `Could not open ${opts.url}.`);\n }\n if (res.status >= 400 && res.status < 500) {\n // Wrapped so the catch below can tell it apart from a transient\n // failure and rethrow instead of reconnecting forever.\n await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {\n throw new Fatal(err);\n });\n return;\n }\n throw new Error(`stream open failed with HTTP ${res.status}`);\n }\n\n for await (const frame of frames(res.body, connection.signal, armIdle)) {\n const parsed = parseFrame<T>(frame, opts.event);\n if (parsed === null) continue;\n if (parsed.id !== null) cursor.lastEventId = parsed.id;\n opts.onEvent(parsed.data, parsed.event);\n madeProgress = true;\n }\n if (wentIdle) throw new SseIdleTimeout(opts.url, idleMs);\n } catch (err) {\n if (opts.signal.aborted) return;\n // A deleted conversation, or a credential a refresh didn't fix.\n if (err instanceof Fatal) throw err.cause;\n // Everything else is transient (network, 5xx, proxy hangup, a socket that\n // went silent) — report and back off rather than end the subscription.\n opts.onError?.(wentIdle ? new SseIdleTimeout(opts.url, idleMs) : err);\n } finally {\n clearTimeout(idleTimer);\n unlink();\n // Nothing below this point reads from the body; letting it hang open\n // would leak a connection per reconnect.\n connection.abort();\n opts.onClose?.();\n }\n\n if (opts.signal.aborted) return;\n\n // Any delivered frame resets the backoff: a long-lived stream that did real\n // work and then dropped should come back fast, not wait out the ceiling.\n if (madeProgress) attempt = 0;\n const delayMs = retryAfterMs ?? Math.min(MAX_BACKOFF_MS, 1_000 * 2 ** attempt);\n attempt += 1;\n await sleep(delayMs, opts.signal);\n }\n}\n\n/** `onRead` fires for every chunk the socket delivers, keep-alive comments\n * included — the watchdog is asking whether the connection is alive, which is\n * not the same question as whether a frame the caller wanted came through. */\nasync function* frames(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n onRead: () => void,\n): AsyncGenerator<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n while (!signal.aborted) {\n const { value, done } = await reader.read();\n if (done) return;\n onRead();\n buffer += decoder.decode(value, { stream: true });\n for (;;) {\n const sep = buffer.indexOf(\"\\n\\n\");\n if (sep === -1) break;\n yield buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n }\n }\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n\nexport function parseFrame<T>(\n frame: string,\n expectedEvent: string | string[],\n): { data: T; id: string | null; event: string } | null {\n let dataLine: string | null = null;\n let id: string | null = null;\n let eventName = \"message\";\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\":\")) continue;\n if (line.startsWith(\"data:\")) dataLine = line.slice(5).trimStart();\n else if (line.startsWith(\"event:\")) eventName = line.slice(6).trim();\n else if (line.startsWith(\"id:\")) id = line.slice(3).trim();\n }\n const wanted =\n typeof expectedEvent === \"string\"\n ? eventName === expectedEvent\n : expectedEvent.includes(eventName);\n if (!wanted || dataLine === null) return null;\n try {\n return { data: JSON.parse(dataLine) as T, id, event: eventName };\n } catch {\n return null;\n }\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** Aborts `child` when `outer` does, and hands back the unsubscribe — without\n * it every reconnect would leave another listener on a signal that lives as\n * long as the subscription. */\nfunction forward(outer: AbortSignal, child: AbortController): () => void {\n const onAbort = () => child.abort();\n if (outer.aborted) child.abort();\n else outer.addEventListener(\"abort\", onAbort, { once: true });\n return () => outer.removeEventListener(\"abort\", onAbort);\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAGO,MAAM,mBAAmB,MAAM;AAAA,EACpC,WAAW,CAAC,SAAiB;AAAA,IAC3B,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA;AAEhB;AAAA;AAGO,MAAM,sBAAsB,WAAW;AAAA,EACnC;AAAA,EAEA;AAAA,EAIA;AAAA,EAET,WAAW,CAAC,SAAiB,QAAgB,YAA2B,MAAM,OAAO,IAAI;AAAA,IACvF,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,KAAK,SAAS;AAAA,IACd,KAAK,YAAY;AAAA,IACjB,KAAK,OAAO;AAAA;AAAA,EAId,IAAiB,GAAkB;AAAA,IACjC,IAAI;AAAA,MACF,OAAO,KAAK,MAAM,KAAK,IAAI;AAAA,MAC3B,MAAM;AAAA,MACN;AAAA;AAAA;AAAA,MAMA,WAAW,GAAY;AAAA,IACzB,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW;AAAA;AAAA,MAG5C,UAAU,GAAY;AAAA,IACxB,OAAO,KAAK,WAAW;AAAA;AAAA,MAKrB,UAAU,GAAY;AAAA,IACxB,OAAO,KAAK,WAAW;AAAA;AAAA,MAIrB,WAAW,GAAY;AAAA,IACzB,OAAO,KAAK,WAAW,OAAO,KAAK,UAAU;AAAA;AAEjD;AAAA;AAOO,MAAM,yBAAyB,WAAW;AAAA,EAC/C,WAAW,CAAC,SAAiB;AAAA,IAC3B,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA;AAEhB;AAAA;AAUO,MAAM,0BAA0B,WAAW;AAAA,EAEvC;AAAA,EAEA;AAAA,EAET,WAAW,CAAC,SAAiB,OAAgB,WAAW,OAAO;AAAA,IAC7D,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,KAAK,WAAW;AAAA;AAEpB;AAEA,IAAM,mBAA2C;AAAA,EAC/C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEA,eAAsB,cAAc,CAAC,KAAe,UAAiC;AAAA,EACnF,IAAI,IAAI;AAAA,IAAI;AAAA,EACZ,IAAI,SAAS;AAAA,EACb,IAAI;AAAA,IACF,SAAS,MAAM,IAAI,KAAK;AAAA,IACxB,MAAM;AAAA,IACN,SAAS;AAAA;AAAA,EAEX,MAAM,OAAO,iBAAiB,IAAI,WAAW;AAAA,EAC7C,MAAM,IAAI,cACR,SAAS,GAAG,SAAS,OAAO,MAAM,GAAG,GAAG,OAAO,MAC/C,IAAI,QACJ,IAAI,QAAQ,IAAI,cAAc,GAC9B,MACF;AAAA;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "3ACA84B6D428D3C464756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|