@codespar/sdk 0.11.0 → 0.13.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/README.md +69 -0
- package/dist/__tests__/abort.test.d.ts +2 -0
- package/dist/__tests__/abort.test.d.ts.map +1 -0
- package/dist/__tests__/abort.test.js +70 -0
- package/dist/__tests__/abort.test.js.map +1 -0
- package/dist/__tests__/api-client.test.d.ts +7 -0
- package/dist/__tests__/api-client.test.d.ts.map +1 -0
- package/dist/__tests__/api-client.test.js +294 -0
- package/dist/__tests__/api-client.test.js.map +1 -0
- package/dist/__tests__/close-timeout.test.d.ts +2 -0
- package/dist/__tests__/close-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/close-timeout.test.js +57 -0
- package/dist/__tests__/close-timeout.test.js.map +1 -0
- package/dist/__tests__/codespar.test.js +11 -0
- package/dist/__tests__/codespar.test.js.map +1 -1
- package/dist/__tests__/connections-timeout.test.d.ts +2 -0
- package/dist/__tests__/connections-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/connections-timeout.test.js +58 -0
- package/dist/__tests__/connections-timeout.test.js.map +1 -0
- package/dist/__tests__/fetch-timeout.test.d.ts +2 -0
- package/dist/__tests__/fetch-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-timeout.test.js +137 -0
- package/dist/__tests__/fetch-timeout.test.js.map +1 -0
- package/dist/__tests__/openapi-snapshot.test.d.ts +13 -0
- package/dist/__tests__/openapi-snapshot.test.d.ts.map +1 -0
- package/dist/__tests__/openapi-snapshot.test.js +79 -0
- package/dist/__tests__/openapi-snapshot.test.js.map +1 -0
- package/dist/__tests__/shop.test.js +1 -1
- package/dist/__tests__/shop.test.js.map +1 -1
- package/dist/__tests__/stream-timeout.test.d.ts +2 -0
- package/dist/__tests__/stream-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/stream-timeout.test.js +335 -0
- package/dist/__tests__/stream-timeout.test.js.map +1 -0
- package/dist/api/client.d.ts +56 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +221 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/types.d.ts +110 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +10 -0
- package/dist/api/types.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/generated/openapi.d.ts +22942 -0
- package/dist/generated/openapi.d.ts.map +1 -0
- package/dist/generated/openapi.js +2 -0
- package/dist/generated/openapi.js.map +1 -0
- package/dist/generated/operations.d.ts +1114 -0
- package/dist/generated/operations.d.ts.map +1 -0
- package/dist/generated/operations.js +231 -0
- package/dist/generated/operations.js.map +1 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/abort.d.ts +18 -0
- package/dist/internal/abort.d.ts.map +1 -0
- package/dist/internal/abort.js +42 -0
- package/dist/internal/abort.js.map +1 -0
- package/dist/internal/fetch.d.ts +23 -0
- package/dist/internal/fetch.d.ts.map +1 -0
- package/dist/internal/fetch.js +57 -0
- package/dist/internal/fetch.js.map +1 -0
- package/dist/session.d.ts +3 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +339 -161
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -3
package/dist/session.js
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session implementation for the CodeSpar managed runtime.
|
|
3
3
|
*/
|
|
4
|
-
import { networkErrorToApiError, throwFromResponse } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
4
|
+
import { CodesparApiError, networkErrorToApiError, throwFromResponse, TimeoutError } from "./errors.js";
|
|
5
|
+
import { fetchWithTimeout, validateTimeout } from "./internal/fetch.js";
|
|
6
|
+
import { mergeSignals } from "./internal/abort.js";
|
|
7
|
+
// Every unary transport site funnels through here. The `consume`
|
|
8
|
+
// callback reads the body inside the timeout/abort budget (via
|
|
9
|
+
// fetchWithTimeout), so a backend that stalls the body still hits the
|
|
10
|
+
// deadline. Network rejections become CodesparApiError (status: 0);
|
|
11
|
+
// typed timeouts and caller aborts propagate verbatim; structured API
|
|
12
|
+
// errors raised inside `consume` (throwFromResponse) pass through
|
|
13
|
+
// unwrapped. Streaming sites use raw fetch (SSE idle-timeout lives at
|
|
14
|
+
// the stream layer), not this helper.
|
|
15
|
+
async function safeFetch(input, init, what, timeoutOpts, consume) {
|
|
10
16
|
try {
|
|
11
|
-
return await
|
|
17
|
+
return await fetchWithTimeout(input, init, timeoutOpts, consume);
|
|
12
18
|
}
|
|
13
19
|
catch (cause) {
|
|
20
|
+
if (cause instanceof TimeoutError)
|
|
21
|
+
throw cause;
|
|
22
|
+
if (cause instanceof CodesparApiError)
|
|
23
|
+
throw cause;
|
|
24
|
+
if (timeoutOpts.signal?.aborted)
|
|
25
|
+
throw cause;
|
|
14
26
|
throw networkErrorToApiError(cause, what);
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
29
|
// The concrete session object satisfies the Session interface and carries extra
|
|
18
30
|
// internal methods (tools, findTools) that the free functions in tools.ts access
|
|
19
31
|
// via duck-typing. They are intentionally not declared on the Session interface.
|
|
20
|
-
export async function createSession(userId, config, deps) {
|
|
32
|
+
export async function createSession(userId, config, deps, createOpts) {
|
|
21
33
|
const { baseUrl, apiKey } = deps;
|
|
22
34
|
const projectId = config.projectId ?? deps.projectId;
|
|
23
35
|
const headers = {
|
|
@@ -31,6 +43,12 @@ export async function createSession(userId, config, deps) {
|
|
|
31
43
|
metadata: config.metadata,
|
|
32
44
|
projectId: config.projectId ?? deps.projectId,
|
|
33
45
|
};
|
|
46
|
+
// Resolve per-call timeout/abort against the client default. Kept local
|
|
47
|
+
// so every method funnels through one place if CallOptions grows.
|
|
48
|
+
const callOpts = (o) => ({
|
|
49
|
+
timeout: o?.timeout ?? deps.timeout,
|
|
50
|
+
signal: o?.signal,
|
|
51
|
+
});
|
|
34
52
|
// Conditional spread keeps the wire body byte-identical to the
|
|
35
53
|
// pre-PRD shape when the caller omits mocks (R18 wire-neutrality).
|
|
36
54
|
// The field is forwarded verbatim — no canonical-name rewriting on
|
|
@@ -42,15 +60,15 @@ export async function createSession(userId, config, deps) {
|
|
|
42
60
|
user_id: userId,
|
|
43
61
|
...(config.mocks !== undefined ? { mocks: config.mocks } : {}),
|
|
44
62
|
};
|
|
45
|
-
const
|
|
63
|
+
const data = await safeFetch(`${baseUrl}/v1/sessions`, {
|
|
46
64
|
method: "POST",
|
|
47
65
|
headers,
|
|
48
66
|
body: JSON.stringify(wireBody),
|
|
49
|
-
}, "createSession")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
}, "createSession", callOpts(createOpts), async (res) => {
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
await throwFromResponse(res, "createSession");
|
|
70
|
+
return (await res.json());
|
|
71
|
+
});
|
|
54
72
|
let cachedTools = null;
|
|
55
73
|
let cachedConnections = null;
|
|
56
74
|
const session = {
|
|
@@ -80,29 +98,29 @@ export async function createSession(userId, config, deps) {
|
|
|
80
98
|
return all.filter((t) => t.name.toLowerCase().includes(q) ||
|
|
81
99
|
t.description.toLowerCase().includes(q));
|
|
82
100
|
},
|
|
83
|
-
async execute(toolName, params) {
|
|
101
|
+
async execute(toolName, params, opts) {
|
|
84
102
|
const start = Date.now();
|
|
85
|
-
|
|
103
|
+
return await safeFetch(`${baseUrl}/v1/sessions/${data.id}/execute`, {
|
|
86
104
|
method: "POST",
|
|
87
105
|
headers,
|
|
88
106
|
body: JSON.stringify({ tool: toolName, input: params }),
|
|
89
|
-
}, "execute")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
}, "execute", callOpts(opts), async (r) => {
|
|
108
|
+
if (!r.ok) {
|
|
109
|
+
const body = await r.text();
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
data: null,
|
|
113
|
+
error: `${r.status}: ${body}`,
|
|
114
|
+
duration: Date.now() - start,
|
|
115
|
+
server: "",
|
|
116
|
+
tool: toolName,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return (await r.json());
|
|
120
|
+
});
|
|
103
121
|
},
|
|
104
|
-
async proxyExecute(request) {
|
|
105
|
-
|
|
122
|
+
async proxyExecute(request, opts) {
|
|
123
|
+
return await safeFetch(`${baseUrl}/v1/sessions/${data.id}/proxy_execute`, {
|
|
106
124
|
method: "POST",
|
|
107
125
|
headers,
|
|
108
126
|
body: JSON.stringify({
|
|
@@ -113,45 +131,72 @@ export async function createSession(userId, config, deps) {
|
|
|
113
131
|
params: request.params,
|
|
114
132
|
headers: request.headers,
|
|
115
133
|
}),
|
|
116
|
-
}, "proxyExecute")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
}, "proxyExecute", callOpts(opts), async (r) => {
|
|
135
|
+
if (!r.ok)
|
|
136
|
+
await throwFromResponse(r, "proxyExecute");
|
|
137
|
+
return (await r.json());
|
|
138
|
+
});
|
|
121
139
|
},
|
|
122
|
-
async send(message) {
|
|
123
|
-
|
|
140
|
+
async send(message, opts) {
|
|
141
|
+
return await safeFetch(`${baseUrl}/v1/sessions/${data.id}/send`, {
|
|
124
142
|
method: "POST",
|
|
125
143
|
headers: { ...headers, Accept: "application/json" },
|
|
126
144
|
body: JSON.stringify({ message }),
|
|
127
|
-
}, "send")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
}, "send", callOpts(opts), async (r) => {
|
|
146
|
+
if (!r.ok)
|
|
147
|
+
await throwFromResponse(r, "send");
|
|
148
|
+
return (await r.json());
|
|
149
|
+
});
|
|
132
150
|
},
|
|
133
|
-
async *sendStream(message) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
async *sendStream(message, opts) {
|
|
152
|
+
const ms = opts?.timeout ?? deps.timeout;
|
|
153
|
+
validateTimeout(ms);
|
|
154
|
+
const idleAc = new AbortController();
|
|
155
|
+
const merged = mergeSignals([idleAc.signal, opts?.signal]);
|
|
156
|
+
const idle = makeIdle(idleAc, ms);
|
|
157
|
+
try {
|
|
158
|
+
// Arm the timeout before the fetch so the connect/headers phase
|
|
159
|
+
// is covered too — not just post-body idle. Otherwise a backend
|
|
160
|
+
// that accepts the socket but never sends headers hangs forever.
|
|
161
|
+
idle.reset();
|
|
162
|
+
const r = await fetch(`${baseUrl}/v1/sessions/${data.id}/send`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
165
|
+
body: JSON.stringify({ message }),
|
|
166
|
+
signal: merged.signal,
|
|
167
|
+
});
|
|
168
|
+
if (!r.ok || !r.body) {
|
|
169
|
+
await throwFromResponse(r, "sendStream");
|
|
170
|
+
}
|
|
171
|
+
// r.body is non-null here — throwFromResponse always throws.
|
|
172
|
+
yield* parseSseStream(r.body, idle, merged.signal);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (opts?.signal?.aborted)
|
|
176
|
+
throw opts.signal.reason;
|
|
177
|
+
if (idleAc.signal.aborted)
|
|
178
|
+
throw new TimeoutError(ms);
|
|
179
|
+
// Structured API errors propagate; only raw transport rejections
|
|
180
|
+
// collapse to CodesparApiError(status: 0), matching safeFetch.
|
|
181
|
+
if (err instanceof CodesparApiError)
|
|
182
|
+
throw err;
|
|
183
|
+
throw networkErrorToApiError(err, "sendStream");
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
idle.pause();
|
|
187
|
+
merged.cleanup();
|
|
141
188
|
}
|
|
142
|
-
// Once we're past the !r.ok branch, r.body is guaranteed non-null.
|
|
143
|
-
yield* parseSseStream(r.body);
|
|
144
189
|
},
|
|
145
190
|
/**
|
|
146
191
|
* codespar_discover wrapper. Same wire as
|
|
147
192
|
* `execute("codespar_discover", {...})` but returns a typed
|
|
148
193
|
* DiscoverResult so the caller doesn't have to cast.
|
|
149
194
|
*/
|
|
150
|
-
async discover(useCase, options) {
|
|
195
|
+
async discover(useCase, options, opts) {
|
|
151
196
|
const result = await session.execute("codespar_discover", {
|
|
152
197
|
use_case: useCase,
|
|
153
198
|
...(options ?? {}),
|
|
154
|
-
});
|
|
199
|
+
}, opts);
|
|
155
200
|
if (!result.success) {
|
|
156
201
|
throw new Error(`discover failed: ${result.error ?? "unknown"}`);
|
|
157
202
|
}
|
|
@@ -165,8 +210,8 @@ export async function createSession(userId, config, deps) {
|
|
|
165
210
|
* when no server_id is given, "status" otherwise (matching the
|
|
166
211
|
* backend default).
|
|
167
212
|
*/
|
|
168
|
-
async connectionWizard(options) {
|
|
169
|
-
const result = await session.execute("codespar_manage_connections", options);
|
|
213
|
+
async connectionWizard(options, opts) {
|
|
214
|
+
const result = await session.execute("codespar_manage_connections", options, opts);
|
|
170
215
|
if (!result.success) {
|
|
171
216
|
throw new Error(`connectionWizard failed: ${result.error ?? "unknown"}`);
|
|
172
217
|
}
|
|
@@ -179,8 +224,8 @@ export async function createSession(userId, config, deps) {
|
|
|
179
224
|
* ToolResult.data. Distinct from the legacy `codespar_pay` rail
|
|
180
225
|
* (which routes to outbound transfers/payouts).
|
|
181
226
|
*/
|
|
182
|
-
async charge(args) {
|
|
183
|
-
const result = await session.execute("codespar_charge", args);
|
|
227
|
+
async charge(args, opts) {
|
|
228
|
+
const result = await session.execute("codespar_charge", args, opts);
|
|
184
229
|
if (!result.success) {
|
|
185
230
|
throw new Error(`charge failed: ${result.error ?? "unknown"}`);
|
|
186
231
|
}
|
|
@@ -193,8 +238,8 @@ export async function createSession(userId, config, deps) {
|
|
|
193
238
|
* ShipResult so the caller doesn't have to cast through
|
|
194
239
|
* ToolResult.data.
|
|
195
240
|
*/
|
|
196
|
-
async ship(args) {
|
|
197
|
-
const result = await session.execute("codespar_ship", args);
|
|
241
|
+
async ship(args, opts) {
|
|
242
|
+
const result = await session.execute("codespar_ship", args, opts);
|
|
198
243
|
if (!result.success) {
|
|
199
244
|
throw new Error(`ship failed: ${result.error ?? "unknown"}`);
|
|
200
245
|
}
|
|
@@ -208,8 +253,8 @@ export async function createSession(userId, config, deps) {
|
|
|
208
253
|
* LedgerResult so the caller doesn't have to cast through
|
|
209
254
|
* ToolResult.data.
|
|
210
255
|
*/
|
|
211
|
-
async ledger(args) {
|
|
212
|
-
const result = await session.execute("codespar_ledger", args);
|
|
256
|
+
async ledger(args, opts) {
|
|
257
|
+
const result = await session.execute("codespar_ledger", args, opts);
|
|
213
258
|
if (!result.success) {
|
|
214
259
|
throw new Error(`ledger failed: ${result.error ?? "unknown"}`);
|
|
215
260
|
}
|
|
@@ -221,8 +266,8 @@ export async function createSession(userId, config, deps) {
|
|
|
221
266
|
* Pomelo card-issuing program. Same wire as
|
|
222
267
|
* `execute("codespar_issue", {...})` but returns a typed IssueResult.
|
|
223
268
|
*/
|
|
224
|
-
async issue(args) {
|
|
225
|
-
const result = await session.execute("codespar_issue", args);
|
|
269
|
+
async issue(args, opts) {
|
|
270
|
+
const result = await session.execute("codespar_issue", args, opts);
|
|
226
271
|
if (!result.success) {
|
|
227
272
|
throw new Error(`issue failed: ${result.error ?? "unknown"}`);
|
|
228
273
|
}
|
|
@@ -241,81 +286,123 @@ export async function createSession(userId, config, deps) {
|
|
|
241
286
|
* (a registered implementation behind the contract); a self-hosted
|
|
242
287
|
* OSS runtime with none returns "Tool not registered".
|
|
243
288
|
*/
|
|
244
|
-
async shop(args) {
|
|
245
|
-
const result = await session.execute("codespar_shop", args);
|
|
289
|
+
async shop(args, opts) {
|
|
290
|
+
const result = await session.execute("codespar_shop", args, opts);
|
|
246
291
|
if (!result.success) {
|
|
247
292
|
throw new Error(`shop failed: ${result.error ?? "unknown"}`);
|
|
248
293
|
}
|
|
249
294
|
return result.data;
|
|
250
295
|
},
|
|
251
|
-
async paymentStatus(toolCallId) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
async paymentStatus(toolCallId, opts) {
|
|
297
|
+
return await safeFetch(`${baseUrl}/v1/tool-calls/${encodeURIComponent(toolCallId)}/payment-status`, { headers }, "paymentStatus", callOpts(opts), async (r) => {
|
|
298
|
+
if (!r.ok)
|
|
299
|
+
await throwFromResponse(r, "paymentStatus");
|
|
300
|
+
return (await r.json());
|
|
301
|
+
});
|
|
257
302
|
},
|
|
258
|
-
async verificationStatus(toolCallId) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
async verificationStatus(toolCallId, opts) {
|
|
304
|
+
return await safeFetch(`${baseUrl}/v1/tool-calls/${encodeURIComponent(toolCallId)}/verification-status`, { headers }, "verificationStatus", callOpts(opts), async (r) => {
|
|
305
|
+
if (!r.ok)
|
|
306
|
+
await throwFromResponse(r, "verificationStatus");
|
|
307
|
+
return (await r.json());
|
|
308
|
+
});
|
|
264
309
|
},
|
|
265
310
|
async paymentStatusStream(toolCallId, options = {}) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
const ms = options.timeout ?? deps.timeout;
|
|
312
|
+
validateTimeout(ms);
|
|
313
|
+
const idleAc = new AbortController();
|
|
314
|
+
const merged = mergeSignals([idleAc.signal, options.signal]);
|
|
315
|
+
const idle = makeIdle(idleAc, ms);
|
|
316
|
+
try {
|
|
317
|
+
// Arm the timeout before the fetch so connect/headers are covered.
|
|
318
|
+
idle.reset();
|
|
319
|
+
const url = `${baseUrl}/v1/tool-calls/${encodeURIComponent(toolCallId)}/payment-status/stream`;
|
|
320
|
+
const r = await fetch(url, {
|
|
321
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
322
|
+
signal: merged.signal,
|
|
323
|
+
});
|
|
324
|
+
if (!r.ok || !r.body) {
|
|
325
|
+
await throwFromResponse(r, "paymentStatusStream");
|
|
326
|
+
}
|
|
327
|
+
let last = null;
|
|
328
|
+
// r.body is non-null here — throwFromResponse always throws.
|
|
329
|
+
for await (const frame of parseStatusSseStream(r.body, idle, merged.signal)) {
|
|
330
|
+
if (frame.event === "snapshot" || frame.event === "update") {
|
|
331
|
+
last = frame.data;
|
|
332
|
+
options.onUpdate?.(last);
|
|
333
|
+
}
|
|
334
|
+
else if (frame.event === "done") {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
281
337
|
}
|
|
282
|
-
|
|
283
|
-
|
|
338
|
+
if (!last) {
|
|
339
|
+
throw new Error("paymentStatusStream: stream closed before snapshot");
|
|
284
340
|
}
|
|
341
|
+
return last;
|
|
285
342
|
}
|
|
286
|
-
|
|
287
|
-
|
|
343
|
+
catch (err) {
|
|
344
|
+
if (options.signal?.aborted)
|
|
345
|
+
throw options.signal.reason;
|
|
346
|
+
if (idleAc.signal.aborted)
|
|
347
|
+
throw new TimeoutError(ms);
|
|
348
|
+
if (err instanceof CodesparApiError)
|
|
349
|
+
throw err;
|
|
350
|
+
throw networkErrorToApiError(err, "paymentStatusStream");
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
idle.pause();
|
|
354
|
+
merged.cleanup();
|
|
288
355
|
}
|
|
289
|
-
return last;
|
|
290
356
|
},
|
|
291
357
|
async verificationStatusStream(toolCallId, options = {}) {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
358
|
+
const ms = options.timeout ?? deps.timeout;
|
|
359
|
+
validateTimeout(ms);
|
|
360
|
+
const idleAc = new AbortController();
|
|
361
|
+
const merged = mergeSignals([idleAc.signal, options.signal]);
|
|
362
|
+
const idle = makeIdle(idleAc, ms);
|
|
363
|
+
try {
|
|
364
|
+
// Arm the timeout before the fetch so connect/headers are covered.
|
|
365
|
+
idle.reset();
|
|
366
|
+
const url = `${baseUrl}/v1/tool-calls/${encodeURIComponent(toolCallId)}/verification-status/stream`;
|
|
367
|
+
const r = await fetch(url, {
|
|
368
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
369
|
+
signal: merged.signal,
|
|
370
|
+
});
|
|
371
|
+
if (!r.ok || !r.body) {
|
|
372
|
+
await throwFromResponse(r, "verificationStatusStream");
|
|
373
|
+
}
|
|
374
|
+
let last = null;
|
|
375
|
+
// r.body is non-null here — throwFromResponse always throws.
|
|
376
|
+
for await (const frame of parseStatusSseStream(r.body, idle, merged.signal)) {
|
|
377
|
+
if (frame.event === "snapshot" || frame.event === "update") {
|
|
378
|
+
last = frame.data;
|
|
379
|
+
options.onUpdate?.(last);
|
|
380
|
+
}
|
|
381
|
+
else if (frame.event === "done") {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
307
384
|
}
|
|
308
|
-
|
|
309
|
-
|
|
385
|
+
if (!last) {
|
|
386
|
+
throw new Error("verificationStatusStream: stream closed before snapshot");
|
|
310
387
|
}
|
|
388
|
+
return last;
|
|
311
389
|
}
|
|
312
|
-
|
|
313
|
-
|
|
390
|
+
catch (err) {
|
|
391
|
+
if (options.signal?.aborted)
|
|
392
|
+
throw options.signal.reason;
|
|
393
|
+
if (idleAc.signal.aborted)
|
|
394
|
+
throw new TimeoutError(ms);
|
|
395
|
+
if (err instanceof CodesparApiError)
|
|
396
|
+
throw err;
|
|
397
|
+
throw networkErrorToApiError(err, "verificationStatusStream");
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
idle.pause();
|
|
401
|
+
merged.cleanup();
|
|
314
402
|
}
|
|
315
|
-
return last;
|
|
316
403
|
},
|
|
317
|
-
async authorize(serverId, config) {
|
|
318
|
-
const
|
|
404
|
+
async authorize(serverId, config, opts) {
|
|
405
|
+
const payload = await safeFetch(`${baseUrl}/v1/connect/start`, {
|
|
319
406
|
method: "POST",
|
|
320
407
|
headers,
|
|
321
408
|
body: JSON.stringify({
|
|
@@ -324,43 +411,59 @@ export async function createSession(userId, config, deps) {
|
|
|
324
411
|
redirect_uri: config.redirectUri,
|
|
325
412
|
scopes: config.scopes,
|
|
326
413
|
}),
|
|
327
|
-
}, "authorize")
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
414
|
+
}, "authorize", callOpts(opts), async (r) => {
|
|
415
|
+
if (!r.ok)
|
|
416
|
+
await throwFromResponse(r, "authorize");
|
|
417
|
+
return (await r.json());
|
|
418
|
+
});
|
|
332
419
|
return {
|
|
333
420
|
linkToken: payload.link_token,
|
|
334
421
|
authorizeUrl: payload.authorize_url,
|
|
335
422
|
expiresAt: payload.expires_at,
|
|
336
423
|
};
|
|
337
424
|
},
|
|
338
|
-
async connections() {
|
|
425
|
+
async connections(opts) {
|
|
426
|
+
// Caller misconfiguration is NOT best-effort — validate the
|
|
427
|
+
// effective timeout before the swallow (parity with close()), so
|
|
428
|
+
// an invalid value surfaces instead of silently returning the
|
|
429
|
+
// cached list.
|
|
430
|
+
const resolved = callOpts(opts);
|
|
431
|
+
validateTimeout(resolved.timeout);
|
|
339
432
|
// Best-effort — both transport failures (CodesparApiError from
|
|
340
433
|
// safeFetch) and non-2xx responses fall back to the cached
|
|
341
434
|
// payload so a transient blip doesn't crater the session.
|
|
342
435
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
436
|
+
return await safeFetch(`${baseUrl}/v1/sessions/${data.id}/connections`, { headers }, "connections", resolved, async (r) => {
|
|
437
|
+
if (!r.ok)
|
|
438
|
+
return cachedConnections ?? [];
|
|
439
|
+
const payload = (await r.json());
|
|
440
|
+
cachedConnections = payload.servers;
|
|
441
|
+
cachedTools = payload.tools;
|
|
442
|
+
return payload.servers;
|
|
443
|
+
});
|
|
350
444
|
}
|
|
351
445
|
catch {
|
|
352
446
|
return cachedConnections ?? [];
|
|
353
447
|
}
|
|
354
448
|
},
|
|
355
|
-
async close() {
|
|
356
|
-
//
|
|
357
|
-
//
|
|
449
|
+
async close(opts) {
|
|
450
|
+
// Caller misconfiguration is NOT best-effort — validate the
|
|
451
|
+
// effective timeout before the swallow, so an invalid value
|
|
452
|
+
// surfaces (parity with Python's ConfigError) instead of
|
|
453
|
+
// silently skipping the DELETE.
|
|
454
|
+
const { timeout } = callOpts(opts);
|
|
455
|
+
validateTimeout(timeout);
|
|
456
|
+
// Best-effort: the DELETE is bounded by the timeout/abort budget
|
|
457
|
+
// so close() can't hang, but its outcome is swallowed so a slow
|
|
458
|
+
// or failing backend cleanup never throws from a caller's
|
|
459
|
+
// teardown/finally. Parity with the Python client (suppresses
|
|
460
|
+
// ApiError + TimeoutError) and the managed-agents adapter (close
|
|
461
|
+
// never throws). The backend reaps stale sessions on a timer.
|
|
358
462
|
try {
|
|
359
|
-
await safeFetch(`${baseUrl}/v1/sessions/${data.id}`, { method: "DELETE", headers }, "close");
|
|
463
|
+
await safeFetch(`${baseUrl}/v1/sessions/${data.id}`, { method: "DELETE", headers }, "close", callOpts(opts), async () => undefined);
|
|
360
464
|
}
|
|
361
465
|
catch {
|
|
362
|
-
//
|
|
363
|
-
// contract; close() never threw.
|
|
466
|
+
// intentionally ignored — see above
|
|
364
467
|
}
|
|
365
468
|
},
|
|
366
469
|
};
|
|
@@ -379,24 +482,43 @@ export async function createSession(userId, config, deps) {
|
|
|
379
482
|
}
|
|
380
483
|
return session;
|
|
381
484
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
485
|
+
function makeIdle(idleAc, ms) {
|
|
486
|
+
let timer;
|
|
487
|
+
return {
|
|
488
|
+
reset() {
|
|
489
|
+
if (timer)
|
|
490
|
+
clearTimeout(timer);
|
|
491
|
+
timer = setTimeout(() => idleAc.abort(), ms);
|
|
492
|
+
},
|
|
493
|
+
pause() {
|
|
494
|
+
if (timer) {
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
timer = undefined;
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function* parseSseStream(body, idle, signal) {
|
|
394
502
|
const reader = body.getReader();
|
|
395
503
|
const decoder = new TextDecoder();
|
|
396
504
|
let buffer = "";
|
|
505
|
+
// Single abort promise + single listener per parser call — avoids
|
|
506
|
+
// per-iteration listener accumulation and MaxListeners warnings.
|
|
507
|
+
let abortReject;
|
|
508
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
509
|
+
abortReject = reject;
|
|
510
|
+
});
|
|
511
|
+
const onAbort = () => abortReject(signal.reason);
|
|
512
|
+
if (signal.aborted) {
|
|
513
|
+
queueMicrotask(() => abortReject(signal.reason));
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
517
|
+
}
|
|
397
518
|
try {
|
|
519
|
+
idle.reset();
|
|
398
520
|
while (true) {
|
|
399
|
-
const { done, value } = await reader.read();
|
|
521
|
+
const { done, value } = await Promise.race([reader.read(), abortPromise]);
|
|
400
522
|
if (done)
|
|
401
523
|
break;
|
|
402
524
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -404,18 +526,42 @@ async function* parseSseStream(body) {
|
|
|
404
526
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
405
527
|
const chunk = buffer.slice(0, sep);
|
|
406
528
|
buffer = buffer.slice(sep + 2);
|
|
529
|
+
// A complete SSE comment frame (":..." heartbeat) carries no
|
|
530
|
+
// data but IS protocol liveness — reset idle. Only an
|
|
531
|
+
// incomplete byte trickle (no "\n\n" yet) must still time out.
|
|
532
|
+
if (chunk.startsWith(":")) {
|
|
533
|
+
idle.reset();
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
407
536
|
const event = parseSseChunk(chunk);
|
|
408
|
-
if (event)
|
|
537
|
+
if (event) {
|
|
538
|
+
// Pause while the consumer processes the event so its
|
|
539
|
+
// processing time is not counted as transport idle; re-arm
|
|
540
|
+
// once control returns, before the next read.
|
|
541
|
+
idle.pause();
|
|
409
542
|
yield event;
|
|
543
|
+
idle.reset();
|
|
544
|
+
}
|
|
410
545
|
}
|
|
411
546
|
}
|
|
412
547
|
if (buffer.trim()) {
|
|
413
548
|
const event = parseSseChunk(buffer);
|
|
414
|
-
if (event)
|
|
549
|
+
if (event) {
|
|
550
|
+
// Same invariant as the inner loop: pause the idle timer
|
|
551
|
+
// while the consumer holds control. The stream has ended, so
|
|
552
|
+
// there is no read to re-arm for afterwards.
|
|
553
|
+
idle.pause();
|
|
415
554
|
yield event;
|
|
555
|
+
}
|
|
416
556
|
}
|
|
417
557
|
}
|
|
418
558
|
finally {
|
|
559
|
+
idle.pause();
|
|
560
|
+
signal.removeEventListener("abort", onAbort);
|
|
561
|
+
// Best-effort, NON-blocking: a degraded ReadableStream may return a
|
|
562
|
+
// cancel() promise that never settles; awaiting it here would delay
|
|
563
|
+
// delivery of the very TimeoutError/abort the caller is waiting for.
|
|
564
|
+
void Promise.resolve(reader.cancel()).catch(() => { });
|
|
419
565
|
reader.releaseLock();
|
|
420
566
|
}
|
|
421
567
|
}
|
|
@@ -491,13 +637,27 @@ const PRESET_SERVERS = {
|
|
|
491
637
|
* just need raw `{event, data}` pairs the caller's typed wrapper
|
|
492
638
|
* casts via the route-level envelope shape.
|
|
493
639
|
*/
|
|
494
|
-
async function* parseStatusSseStream(body) {
|
|
640
|
+
async function* parseStatusSseStream(body, idle, signal) {
|
|
495
641
|
const reader = body.getReader();
|
|
496
642
|
const decoder = new TextDecoder();
|
|
497
643
|
let buffer = "";
|
|
644
|
+
// Single abort promise + single listener per parser call — avoids
|
|
645
|
+
// per-iteration listener accumulation and MaxListeners warnings.
|
|
646
|
+
let abortReject;
|
|
647
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
648
|
+
abortReject = reject;
|
|
649
|
+
});
|
|
650
|
+
const onAbort = () => abortReject(signal.reason);
|
|
651
|
+
if (signal.aborted) {
|
|
652
|
+
queueMicrotask(() => abortReject(signal.reason));
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
656
|
+
}
|
|
498
657
|
try {
|
|
658
|
+
idle.reset();
|
|
499
659
|
while (true) {
|
|
500
|
-
const { done, value } = await reader.read();
|
|
660
|
+
const { done, value } = await Promise.race([reader.read(), abortPromise]);
|
|
501
661
|
if (done)
|
|
502
662
|
break;
|
|
503
663
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -505,9 +665,16 @@ async function* parseStatusSseStream(body) {
|
|
|
505
665
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
506
666
|
const chunk = buffer.slice(0, sep);
|
|
507
667
|
buffer = buffer.slice(sep + 2);
|
|
508
|
-
// Comment frames (heartbeats) start with ":" and carry no
|
|
509
|
-
|
|
668
|
+
// Comment frames (heartbeats) start with ":" and carry no
|
|
669
|
+
// data, but a complete heartbeat frame IS protocol liveness —
|
|
670
|
+
// reset idle so healthy long-pending streams don't time out
|
|
671
|
+
// (parity with Python httpx, whose read timeout any byte
|
|
672
|
+
// resets). An incomplete byte trickle still times out because
|
|
673
|
+
// it never forms a "\n\n" frame.
|
|
674
|
+
if (chunk.startsWith(":")) {
|
|
675
|
+
idle.reset();
|
|
510
676
|
continue;
|
|
677
|
+
}
|
|
511
678
|
let eventName = "message";
|
|
512
679
|
let dataLine = "";
|
|
513
680
|
for (const line of chunk.split("\n")) {
|
|
@@ -518,16 +685,27 @@ async function* parseStatusSseStream(body) {
|
|
|
518
685
|
}
|
|
519
686
|
if (!dataLine)
|
|
520
687
|
continue;
|
|
688
|
+
let parsed;
|
|
521
689
|
try {
|
|
522
|
-
|
|
690
|
+
parsed = JSON.parse(dataLine);
|
|
523
691
|
}
|
|
524
692
|
catch {
|
|
525
693
|
continue;
|
|
526
694
|
}
|
|
695
|
+
// Pause while the consumer processes the frame; re-arm after.
|
|
696
|
+
idle.pause();
|
|
697
|
+
yield { event: eventName, data: parsed };
|
|
698
|
+
idle.reset();
|
|
527
699
|
}
|
|
528
700
|
}
|
|
529
701
|
}
|
|
530
702
|
finally {
|
|
703
|
+
idle.pause();
|
|
704
|
+
signal.removeEventListener("abort", onAbort);
|
|
705
|
+
// Best-effort, NON-blocking: a degraded ReadableStream may return a
|
|
706
|
+
// cancel() promise that never settles; awaiting it here would delay
|
|
707
|
+
// delivery of the very TimeoutError/abort the caller is waiting for.
|
|
708
|
+
void Promise.resolve(reader.cancel()).catch(() => { });
|
|
531
709
|
reader.releaseLock();
|
|
532
710
|
}
|
|
533
711
|
}
|