@cubos/agent-sdk 0.0.1140467 → 0.0.1140658
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 +14 -1
- package/dist/client.d.ts +15 -2
- package/dist/http.d.ts +4 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +72 -16
- package/dist/index.js.map +6 -6
- package/dist/sse.d.ts +35 -3
- package/dist/sse.js +55 -8
- package/dist/sse.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -437,7 +437,20 @@ api_keys are exempt from all of this.
|
|
|
437
437
|
|
|
438
438
|
Transient stream failures do not end a subscription: the SDK reconnects with
|
|
439
439
|
exponential backoff and reports them through `onError`, for logging or a
|
|
440
|
-
"reconnecting" hint.
|
|
440
|
+
"reconnecting" hint.
|
|
441
|
+
|
|
442
|
+
Two failures that look like silence get handled rather than reported. A **401**
|
|
443
|
+
buys one immediate reconnect with a forced token refresh — a short-lived token
|
|
444
|
+
expiring under a stream that outlives it is the expected case, not a
|
|
445
|
+
misconfiguration — and only a second rejection is final. A connection that stops
|
|
446
|
+
sending **anything**, keep-alive comments included, is dropped and reopened after
|
|
447
|
+
`idleTimeoutMs` (30s, twice the server's keep-alive interval): a half-open socket
|
|
448
|
+
otherwise leaves the reader waiting forever with no error to react to.
|
|
449
|
+
|
|
450
|
+
What remains final — a 4xx that survived the refresh, a deleted conversation —
|
|
451
|
+
closes the subscription for good and fires **`onFatal`** (`onError` sees it too).
|
|
452
|
+
That is the one a UI has to surface: everything still renders, and nothing will
|
|
453
|
+
ever arrive again.
|
|
441
454
|
|
|
442
455
|
## Also in the package
|
|
443
456
|
|
package/dist/client.d.ts
CHANGED
|
@@ -81,9 +81,20 @@ export interface ConversationHandlers {
|
|
|
81
81
|
* as `since` on the next connect — that is what lets a cache pick up where it
|
|
82
82
|
* left off instead of replaying. */
|
|
83
83
|
onCursor?: (changeSeq: number) => void;
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
84
|
+
/** Every stream failure, including the one that ends the subscription. The
|
|
85
|
+
* SDK is already reconnecting for all but that one, so this is for logging or
|
|
86
|
+
* a "reconnecting…" hint — watch `onFatal` for the end. */
|
|
86
87
|
onError?: (err: unknown) => void;
|
|
88
|
+
/**
|
|
89
|
+
* The subscription has stopped and will not come back: the credential was
|
|
90
|
+
* rejected twice (once after a forced refresh), the conversation is gone, or
|
|
91
|
+
* the server refused the stream outright. Fires at most once.
|
|
92
|
+
*
|
|
93
|
+
* Separate from `onError` because the two need opposite treatment — a failed
|
|
94
|
+
* reconnect is normal and shows at most a hint, while this is a dead view
|
|
95
|
+
* that has to tell the user, and only a fresh `subscribe` revives it.
|
|
96
|
+
*/
|
|
97
|
+
onFatal?: (err: unknown) => void;
|
|
87
98
|
}
|
|
88
99
|
/** What `loadHistory` resolves to: a `MessagePage` plus where it came from. */
|
|
89
100
|
export interface HistoryStart {
|
|
@@ -105,6 +116,8 @@ export interface HistoryStart {
|
|
|
105
116
|
export interface ListHandlers {
|
|
106
117
|
onConversation: (conversation: Conversation) => void;
|
|
107
118
|
onError?: (err: unknown) => void;
|
|
119
|
+
/** As on `ConversationHandlers`: the stream is gone for good. */
|
|
120
|
+
onFatal?: (err: unknown) => void;
|
|
108
121
|
}
|
|
109
122
|
export declare class AgentClient {
|
|
110
123
|
#private;
|
package/dist/http.d.ts
CHANGED
|
@@ -27,8 +27,10 @@ interface RequestOptions {
|
|
|
27
27
|
export interface Transport {
|
|
28
28
|
request<T>(method: string, path: string, opts?: RequestOptions): Promise<T>;
|
|
29
29
|
fetchRaw(method: string, path: string, opts?: RequestOptions): Promise<Response>;
|
|
30
|
-
/** Headers for a stream connect, refreshed per (re)connect attempt.
|
|
31
|
-
|
|
30
|
+
/** Headers for a stream connect, refreshed per (re)connect attempt.
|
|
31
|
+
* `forceRefresh` is the stream's counterpart to the 401 retry `request` does
|
|
32
|
+
* on its own: a `getToken` that caches must mint a new one when it is set. */
|
|
33
|
+
streamHeaders(forceRefresh?: boolean): Promise<Record<string, string>>;
|
|
32
34
|
url(path: string, query?: RequestOptions["query"]): string;
|
|
33
35
|
fetchImpl: FetchLike;
|
|
34
36
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export type { Auth, TokenSource, Transport } from "./http.js";
|
|
|
11
11
|
export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS } from "./http.js";
|
|
12
12
|
export { mergeToolActivities, mergeVoiceMessages } from "./mapping.js";
|
|
13
13
|
export type { Schemas } from "./schemas.js";
|
|
14
|
-
export type { FetchLike } from "./sse.js";
|
|
15
|
-
export { readSse } from "./sse.js";
|
|
14
|
+
export type { FetchLike, SseOptions } from "./sse.js";
|
|
15
|
+
export { DEFAULT_IDLE_TIMEOUT_MS, readSse, SseIdleTimeout } from "./sse.js";
|
|
16
16
|
export type { Activity, Attachment, AttachmentKind, Block, ClientToolCall, Conversation, ConversationEvent, CreateConversationOptions, EnabledComponents, EventPage, Identity, ListConversationsOptions, Message, MessagePage, MessageRole, Page, PlanSnapshot, Todo, TodoStatus, ToolActivity, WorkspaceDir, WorkspaceEntry, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ async function raiseForStatus(res, fallback) {
|
|
|
79
79
|
|
|
80
80
|
// src/sse.ts
|
|
81
81
|
var MAX_BACKOFF_MS = 30000;
|
|
82
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
82
83
|
|
|
83
84
|
class Fatal extends Error {
|
|
84
85
|
cause;
|
|
@@ -87,25 +88,54 @@ class Fatal extends Error {
|
|
|
87
88
|
this.cause = cause;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
|
|
92
|
+
class SseIdleTimeout extends Error {
|
|
93
|
+
constructor(url, ms) {
|
|
94
|
+
super(`Stream ${url} sent nothing for ${ms}ms — reconnecting.`);
|
|
95
|
+
this.name = "SseIdleTimeout";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
90
98
|
async function readSse(opts) {
|
|
91
99
|
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
100
|
+
const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
92
101
|
let lastEventId = opts.lastEventId;
|
|
93
102
|
let attempt = 0;
|
|
103
|
+
let refreshing = false;
|
|
94
104
|
while (!opts.signal.aborted) {
|
|
95
105
|
let madeProgress = false;
|
|
106
|
+
const connection = new AbortController;
|
|
107
|
+
const unlink = forward(opts.signal, connection);
|
|
108
|
+
let idleTimer;
|
|
109
|
+
let wentIdle = false;
|
|
110
|
+
const armIdle = () => {
|
|
111
|
+
if (idleMs <= 0)
|
|
112
|
+
return;
|
|
113
|
+
clearTimeout(idleTimer);
|
|
114
|
+
idleTimer = setTimeout(() => {
|
|
115
|
+
wentIdle = true;
|
|
116
|
+
connection.abort();
|
|
117
|
+
}, idleMs);
|
|
118
|
+
};
|
|
96
119
|
try {
|
|
97
120
|
const headers = {
|
|
98
|
-
...await opts.headers?.(),
|
|
121
|
+
...await opts.headers?.({ forceRefresh: refreshing }),
|
|
99
122
|
Accept: "text/event-stream"
|
|
100
123
|
};
|
|
101
124
|
if (lastEventId !== undefined)
|
|
102
125
|
headers["Last-Event-ID"] = lastEventId;
|
|
103
|
-
|
|
126
|
+
armIdle();
|
|
127
|
+
const res = await doFetch(opts.url, { headers, signal: connection.signal });
|
|
104
128
|
if (opts.signal.aborted)
|
|
105
129
|
return;
|
|
106
|
-
if (res.ok && res.body)
|
|
130
|
+
if (res.ok && res.body) {
|
|
131
|
+
refreshing = false;
|
|
107
132
|
opts.onOpen?.();
|
|
133
|
+
}
|
|
108
134
|
if (!res.ok || !res.body) {
|
|
135
|
+
if (res.status === 401 && opts.headers && !refreshing) {
|
|
136
|
+
refreshing = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
109
139
|
if (res.status >= 400 && res.status < 500) {
|
|
110
140
|
await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {
|
|
111
141
|
throw new Fatal(err);
|
|
@@ -114,7 +144,7 @@ async function readSse(opts) {
|
|
|
114
144
|
}
|
|
115
145
|
throw new Error(`stream open failed with HTTP ${res.status}`);
|
|
116
146
|
}
|
|
117
|
-
for await (const frame of frames(res.body,
|
|
147
|
+
for await (const frame of frames(res.body, connection.signal, armIdle)) {
|
|
118
148
|
const parsed = parseFrame(frame, opts.event);
|
|
119
149
|
if (parsed === null)
|
|
120
150
|
continue;
|
|
@@ -123,12 +153,18 @@ async function readSse(opts) {
|
|
|
123
153
|
opts.onEvent(parsed.data, parsed.event);
|
|
124
154
|
madeProgress = true;
|
|
125
155
|
}
|
|
156
|
+
if (wentIdle)
|
|
157
|
+
throw new SseIdleTimeout(opts.url, idleMs);
|
|
126
158
|
} catch (err) {
|
|
127
159
|
if (opts.signal.aborted)
|
|
128
160
|
return;
|
|
129
161
|
if (err instanceof Fatal)
|
|
130
162
|
throw err.cause;
|
|
131
|
-
opts.onError?.(err);
|
|
163
|
+
opts.onError?.(wentIdle ? new SseIdleTimeout(opts.url, idleMs) : err);
|
|
164
|
+
} finally {
|
|
165
|
+
clearTimeout(idleTimer);
|
|
166
|
+
unlink();
|
|
167
|
+
connection.abort();
|
|
132
168
|
}
|
|
133
169
|
if (opts.signal.aborted)
|
|
134
170
|
return;
|
|
@@ -139,7 +175,7 @@ async function readSse(opts) {
|
|
|
139
175
|
await sleep(delayMs, opts.signal);
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
|
-
async function* frames(body, signal) {
|
|
178
|
+
async function* frames(body, signal, onRead) {
|
|
143
179
|
const reader = body.getReader();
|
|
144
180
|
const decoder = new TextDecoder;
|
|
145
181
|
let buffer = "";
|
|
@@ -148,6 +184,7 @@ async function* frames(body, signal) {
|
|
|
148
184
|
const { value, done } = await reader.read();
|
|
149
185
|
if (done)
|
|
150
186
|
return;
|
|
187
|
+
onRead();
|
|
151
188
|
buffer += decoder.decode(value, { stream: true });
|
|
152
189
|
for (;; ) {
|
|
153
190
|
const sep = buffer.indexOf(`
|
|
@@ -200,6 +237,14 @@ function sleep(ms, signal) {
|
|
|
200
237
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
201
238
|
});
|
|
202
239
|
}
|
|
240
|
+
function forward(outer, child) {
|
|
241
|
+
const onAbort = () => child.abort();
|
|
242
|
+
if (outer.aborted)
|
|
243
|
+
child.abort();
|
|
244
|
+
else
|
|
245
|
+
outer.addEventListener("abort", onAbort, { once: true });
|
|
246
|
+
return () => outer.removeEventListener("abort", onAbort);
|
|
247
|
+
}
|
|
203
248
|
|
|
204
249
|
// src/http.ts
|
|
205
250
|
var DEFAULT_TIMEOUT_MS = 30000;
|
|
@@ -326,8 +371,8 @@ function createTransport(baseUrl, auth, fetchImpl = globalThis.fetch, timeoutMs
|
|
|
326
371
|
return {
|
|
327
372
|
fetchImpl,
|
|
328
373
|
url: (path, query) => `${root}${path}${buildQuery(query)}`,
|
|
329
|
-
async streamHeaders() {
|
|
330
|
-
return { Authorization: await authHeader(
|
|
374
|
+
async streamHeaders(forceRefresh = false) {
|
|
375
|
+
return { Authorization: await authHeader(forceRefresh) };
|
|
331
376
|
},
|
|
332
377
|
fetchRaw: once,
|
|
333
378
|
async request(method, path, opts = {}) {
|
|
@@ -508,7 +553,7 @@ function serveClientTools(t, conversationPath, conversationId, options) {
|
|
|
508
553
|
await readSse({
|
|
509
554
|
url: t.url(`${base}/events/stream`),
|
|
510
555
|
event: "conversation_event",
|
|
511
|
-
headers: () => t.streamHeaders(),
|
|
556
|
+
headers: ({ forceRefresh }) => t.streamHeaders(forceRefresh),
|
|
512
557
|
fetchImpl: t.fetchImpl,
|
|
513
558
|
signal: controller.signal,
|
|
514
559
|
onError: options.onError,
|
|
@@ -1736,7 +1781,7 @@ class AgentClient {
|
|
|
1736
1781
|
url: this.#transport.url(`${base}/${id}/events/stream`),
|
|
1737
1782
|
event: ["conversation_event", "conversation_status"],
|
|
1738
1783
|
lastEventId: opts.since === undefined ? undefined : String(opts.since),
|
|
1739
|
-
headers: () => this.#transport.streamHeaders(),
|
|
1784
|
+
headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
|
|
1740
1785
|
fetchImpl: this.#transport.fetchImpl,
|
|
1741
1786
|
signal: controller.signal,
|
|
1742
1787
|
onError: handlers.onError,
|
|
@@ -1768,18 +1813,24 @@ class AgentClient {
|
|
|
1768
1813
|
handlers.onToolActivity?.(withToolArguments(activity, streamedArgs));
|
|
1769
1814
|
handlers.onCursor?.(raw.change_seq);
|
|
1770
1815
|
}
|
|
1771
|
-
}).catch((err) =>
|
|
1816
|
+
}).catch((err) => {
|
|
1817
|
+
handlers.onError?.(err);
|
|
1818
|
+
handlers.onFatal?.(err);
|
|
1819
|
+
});
|
|
1772
1820
|
}
|
|
1773
1821
|
if (handlers.onConversation) {
|
|
1774
1822
|
readSse({
|
|
1775
1823
|
url: this.#transport.url(`${base}/${id}/meta/stream`),
|
|
1776
1824
|
event: "conversation_meta",
|
|
1777
|
-
headers: () => this.#transport.streamHeaders(),
|
|
1825
|
+
headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
|
|
1778
1826
|
fetchImpl: this.#transport.fetchImpl,
|
|
1779
1827
|
signal: controller.signal,
|
|
1780
1828
|
onError: handlers.onError,
|
|
1781
1829
|
onEvent: (raw) => handlers.onConversation?.(toConversation(raw))
|
|
1782
|
-
}).catch((err) =>
|
|
1830
|
+
}).catch((err) => {
|
|
1831
|
+
handlers.onError?.(err);
|
|
1832
|
+
handlers.onFatal?.(err);
|
|
1833
|
+
});
|
|
1783
1834
|
}
|
|
1784
1835
|
});
|
|
1785
1836
|
return { close: () => controller.abort() };
|
|
@@ -1792,12 +1843,15 @@ class AgentClient {
|
|
|
1792
1843
|
readSse({
|
|
1793
1844
|
url: this.#transport.url(`${base}/stream`, { origin: "interactive" }),
|
|
1794
1845
|
event: "conversation",
|
|
1795
|
-
headers: () => this.#transport.streamHeaders(),
|
|
1846
|
+
headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
|
|
1796
1847
|
fetchImpl: this.#transport.fetchImpl,
|
|
1797
1848
|
signal: controller.signal,
|
|
1798
1849
|
onError: handlers.onError,
|
|
1799
1850
|
onEvent: (raw) => handlers.onConversation(toConversation(raw))
|
|
1800
|
-
}).catch((err) =>
|
|
1851
|
+
}).catch((err) => {
|
|
1852
|
+
handlers.onError?.(err);
|
|
1853
|
+
handlers.onFatal?.(err);
|
|
1854
|
+
});
|
|
1801
1855
|
});
|
|
1802
1856
|
return { close: () => controller.abort() };
|
|
1803
1857
|
}
|
|
@@ -1812,9 +1866,11 @@ export {
|
|
|
1812
1866
|
mergeToolActivities,
|
|
1813
1867
|
createUserClient,
|
|
1814
1868
|
createAdminClient,
|
|
1869
|
+
SseIdleTimeout,
|
|
1815
1870
|
MemoryConversationCache,
|
|
1816
1871
|
DEFAULT_TIMEOUT_MS,
|
|
1817
1872
|
DEFAULT_MAX_RETRIES,
|
|
1873
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
1818
1874
|
AgentNetworkError,
|
|
1819
1875
|
AgentError,
|
|
1820
1876
|
AgentConfigError,
|
|
@@ -1822,5 +1878,5 @@ export {
|
|
|
1822
1878
|
AgentApiError
|
|
1823
1879
|
};
|
|
1824
1880
|
|
|
1825
|
-
//# debugId=
|
|
1881
|
+
//# debugId=609F372BC6CA145264756E2164756E21
|
|
1826
1882
|
//# sourceMappingURL=index.js.map
|