@lionhummer/constellation 0.2.0 → 0.4.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 +145 -23
- package/dist/index.js +573 -545
- package/dist/types/client-core/src/error-details.d.ts +6 -0
- package/dist/{index.d.ts → types/client-core/src/index.d.ts} +107 -213
- package/dist/types/sdk/src/index.d.ts +210 -0
- package/examples/quickstart.mjs +48 -0
- package/package.json +16 -6
package/dist/index.js
CHANGED
|
@@ -1,563 +1,591 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// ../client-core/src/error-details.ts
|
|
2
|
+
function callerErrorDetails(error) {
|
|
3
|
+
let cause = error;
|
|
4
|
+
const seen = new Set;
|
|
5
|
+
for (let depth = 0;depth < 8 && cause instanceof Error && !seen.has(cause); depth++) {
|
|
6
|
+
seen.add(cause);
|
|
7
|
+
if (!(cause.cause instanceof Error))
|
|
8
|
+
break;
|
|
9
|
+
cause = cause.cause;
|
|
10
|
+
}
|
|
11
|
+
const record = cause && typeof cause === "object" ? cause : {};
|
|
12
|
+
const name = cause instanceof Error ? cause.name.slice(0, 80) : "CallerError";
|
|
13
|
+
const code = typeof record.code === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(record.code) ? record.code : undefined;
|
|
14
|
+
const raw = cause instanceof Error ? cause.message : "Caller handler threw a non-Error value";
|
|
15
|
+
const message = (/Failed query:|\bparams:/i.test(raw) ? "Caller database operation failed; see caller logs" : raw).replace(/\b(?:Bearer\s+)[^\s,;]+/gi, "Bearer [redacted]").replace(/((?:password|token|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, "$1[redacted]").replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, "$1[redacted]@").slice(0, 512);
|
|
16
|
+
return { name, message, ...code ? { code } : {} };
|
|
10
17
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
|
|
19
|
+
// ../client-core/src/index.ts
|
|
20
|
+
class ConstellationError extends Error {
|
|
21
|
+
status;
|
|
22
|
+
code;
|
|
23
|
+
constructor(status, code, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = "ConstellationError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function uuid7() {
|
|
31
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
32
|
+
let ms = Date.now();
|
|
33
|
+
for (let i = 5;i >= 0; i--) {
|
|
34
|
+
bytes[i] = ms % 256;
|
|
35
|
+
ms = Math.floor(ms / 256);
|
|
36
|
+
}
|
|
37
|
+
bytes[6] = 112 | bytes[6] & 15;
|
|
38
|
+
bytes[8] = 128 | bytes[8] & 63;
|
|
39
|
+
const h = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
40
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
22
41
|
}
|
|
23
|
-
|
|
42
|
+
var terminal = (s) => ["completed", "failed", "cancelled"].includes(s);
|
|
24
43
|
function queryString(values) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
const params = new URLSearchParams;
|
|
45
|
+
for (const [key, value] of Object.entries(values))
|
|
46
|
+
if (value !== undefined && value !== null)
|
|
47
|
+
params.set(key, String(value));
|
|
48
|
+
return params.size ? `?${params}` : "";
|
|
30
49
|
}
|
|
31
50
|
async function pause(ms, signal) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
const abort = () => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
56
|
+
};
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
signal?.removeEventListener("abort", abort);
|
|
59
|
+
resolve();
|
|
60
|
+
}, ms);
|
|
61
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
62
|
+
});
|
|
44
63
|
}
|
|
45
64
|
async function bounded(promise, signal) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.finally(() => signal.removeEventListener("abort", abort));
|
|
53
|
-
});
|
|
65
|
+
signal.throwIfAborted();
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const abort = () => reject(signal.reason);
|
|
68
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
69
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
70
|
+
});
|
|
54
71
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return this.call(`/v1/requests/${encodeURIComponent(id)}/cancel`, "POST", {}, signal);
|
|
130
|
-
}
|
|
131
|
-
claim(id, inputId, claimId, signal) {
|
|
132
|
-
return this.call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}/claim`, "POST", { claimId }, signal);
|
|
133
|
-
}
|
|
134
|
-
/** Agent claims are leases. Keep one alive while a caller handler is running. */
|
|
135
|
-
maintainClaim(id, inputId, claimId, externalSignal) {
|
|
136
|
-
const controller = new AbortController();
|
|
137
|
-
const signal = externalSignal
|
|
138
|
-
? AbortSignal.any([externalSignal, controller.signal])
|
|
139
|
-
: controller.signal;
|
|
140
|
-
const renewal = (async () => {
|
|
141
|
-
while (!signal.aborted) {
|
|
142
|
-
try {
|
|
143
|
-
await pause(15_000, signal);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (signal.aborted)
|
|
149
|
-
return;
|
|
150
|
-
try {
|
|
151
|
-
await this.claim(id, inputId, claimId, signal);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
controller.abort(error);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
})();
|
|
159
|
-
return {
|
|
160
|
-
signal,
|
|
161
|
-
stop: async () => {
|
|
162
|
-
controller.abort();
|
|
163
|
-
await renewal;
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
async whileClaimed(id, inputId, claimId, leased, operation, signal) {
|
|
168
|
-
const lease = leased ? this.maintainClaim(id, inputId, claimId, signal) : null;
|
|
72
|
+
|
|
73
|
+
class RequestClient {
|
|
74
|
+
#fetcher;
|
|
75
|
+
#options;
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.#options = { ...options };
|
|
78
|
+
this.#fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
79
|
+
}
|
|
80
|
+
#url(path) {
|
|
81
|
+
const route = this.#options.apiPrefix === "/internal" ? path.replace(/^\/v1(?=\/)/, "/internal") : path;
|
|
82
|
+
return `${this.#options.baseUrl.replace(/\/$/, "")}${route}`;
|
|
83
|
+
}
|
|
84
|
+
async#call(path, method = "GET", body, signal) {
|
|
85
|
+
const response = await this.#fetcher(this.#url(path), {
|
|
86
|
+
method,
|
|
87
|
+
headers: {
|
|
88
|
+
...body !== undefined ? { "Content-Type": "application/json" } : {}
|
|
89
|
+
},
|
|
90
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
91
|
+
signal: signal ? AbortSignal.any([
|
|
92
|
+
signal,
|
|
93
|
+
AbortSignal.timeout(this.#options.timeoutMs ?? 30000)
|
|
94
|
+
]) : AbortSignal.timeout(this.#options.timeoutMs ?? 30000)
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const body2 = await response.json().catch(() => ({}));
|
|
98
|
+
throw new ConstellationError(response.status, body2.error?.code ?? "http_error", body2.error?.message ?? `Constellation returned ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
return response.status === 204 ? undefined : response.json();
|
|
101
|
+
}
|
|
102
|
+
#submit(input, signal) {
|
|
103
|
+
return this.#call("/v1/requests", "POST", input, signal);
|
|
104
|
+
}
|
|
105
|
+
#inspect(id, signal) {
|
|
106
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}`, "GET", undefined, signal);
|
|
107
|
+
}
|
|
108
|
+
#timeline(id, options = {}) {
|
|
109
|
+
const params = new URLSearchParams;
|
|
110
|
+
if (options.requestId)
|
|
111
|
+
params.set("requestId", options.requestId);
|
|
112
|
+
const query = params.size ? `?${params}` : "";
|
|
113
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/timeline${query}`, "GET", undefined, options.signal);
|
|
114
|
+
}
|
|
115
|
+
#inspectSummary(id, signal) {
|
|
116
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}?summary=true`, "GET", undefined, signal);
|
|
117
|
+
}
|
|
118
|
+
#records(id, kind, query = {}, signal) {
|
|
119
|
+
const params = new URLSearchParams;
|
|
120
|
+
for (const [key, value] of Object.entries(query))
|
|
121
|
+
if (value !== undefined)
|
|
122
|
+
params.set(key, String(value));
|
|
123
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/records/${kind}?${params}`, "GET", undefined, signal);
|
|
124
|
+
}
|
|
125
|
+
#resolve(input, signal) {
|
|
126
|
+
return this.#call("/v1/requests/resolve", "POST", input, signal);
|
|
127
|
+
}
|
|
128
|
+
#list(query = {}, signal) {
|
|
129
|
+
const params = new URLSearchParams;
|
|
130
|
+
for (const [k, v] of Object.entries(query))
|
|
131
|
+
if (v !== undefined)
|
|
132
|
+
params.set(k, String(v));
|
|
133
|
+
return this.#call(`/v1/requests?${params}`, "GET", undefined, signal);
|
|
134
|
+
}
|
|
135
|
+
#cancel(id, signal) {
|
|
136
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/cancel`, "POST", {}, signal);
|
|
137
|
+
}
|
|
138
|
+
#claim(id, inputId, claimId, signal) {
|
|
139
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}/claim`, "POST", { claimId }, signal);
|
|
140
|
+
}
|
|
141
|
+
maintainClaim(id, inputId, claimId, externalSignal) {
|
|
142
|
+
const controller = new AbortController;
|
|
143
|
+
const signal = externalSignal ? AbortSignal.any([externalSignal, controller.signal]) : controller.signal;
|
|
144
|
+
const renewal = (async () => {
|
|
145
|
+
while (!signal.aborted) {
|
|
169
146
|
try {
|
|
170
|
-
|
|
147
|
+
await pause(15000, signal);
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
171
150
|
}
|
|
172
|
-
|
|
173
|
-
|
|
151
|
+
if (signal.aborted)
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
await this.#claim(id, inputId, claimId, signal);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
controller.abort(error);
|
|
157
|
+
return;
|
|
174
158
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
return {
|
|
162
|
+
signal,
|
|
163
|
+
stop: async () => {
|
|
164
|
+
controller.abort();
|
|
165
|
+
await renewal;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async#whileClaimed(id, inputId, claimId, leased, operation, signal) {
|
|
170
|
+
const lease = leased ? this.maintainClaim(id, inputId, claimId, signal) : null;
|
|
171
|
+
try {
|
|
172
|
+
return await operation(lease?.signal ?? signal);
|
|
173
|
+
} finally {
|
|
174
|
+
await lease?.stop();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
#respond(id, inputId, response, signal) {
|
|
178
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}`, "POST", response, signal);
|
|
179
|
+
}
|
|
180
|
+
#addSpans(id, spans, signal) {
|
|
181
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/spans`, "POST", spans, signal);
|
|
182
|
+
}
|
|
183
|
+
#addLogs(id, logs, signal) {
|
|
184
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/logs`, "POST", logs, signal);
|
|
185
|
+
}
|
|
186
|
+
async#startSpan(id, name, options = {}, signal) {
|
|
187
|
+
const span = {
|
|
188
|
+
id: uuid7(),
|
|
189
|
+
name,
|
|
190
|
+
startedAt: options.startedAt ?? new Date().toISOString(),
|
|
191
|
+
status: "running",
|
|
192
|
+
...options
|
|
193
|
+
};
|
|
194
|
+
await this.#addSpans(id, [span], signal);
|
|
195
|
+
let closed;
|
|
196
|
+
return {
|
|
197
|
+
id: span.id,
|
|
198
|
+
end: (options2 = {}, signal2) => {
|
|
199
|
+
closed = {
|
|
200
|
+
...span,
|
|
201
|
+
endedAt: options2.endedAt ?? closed?.endedAt ?? new Date().toISOString(),
|
|
202
|
+
status: options2.status ?? closed?.status ?? "ok",
|
|
203
|
+
attributes: options2.attributes ?? closed?.attributes ?? span.attributes
|
|
206
204
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
return this.#addSpans(id, [closed], signal2);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async* #events(id, options = {}) {
|
|
210
|
+
let after = options.after ?? 0;
|
|
211
|
+
while (true) {
|
|
212
|
+
options.signal?.throwIfAborted();
|
|
213
|
+
const connecting = new AbortController;
|
|
214
|
+
const connectTimeout = setTimeout(() => connecting.abort(), this.#options.timeoutMs ?? 30000);
|
|
215
|
+
const response = await Promise.resolve().then(() => this.#fetcher(this.#url(`/v1/requests/${encodeURIComponent(id)}/stream?after=${after}`), {
|
|
216
|
+
headers: {
|
|
217
|
+
Accept: "text/event-stream"
|
|
218
|
+
},
|
|
219
|
+
signal: options.signal ? AbortSignal.any([options.signal, connecting.signal]) : connecting.signal
|
|
220
|
+
})).catch(async () => {
|
|
221
|
+
clearTimeout(connectTimeout);
|
|
222
|
+
options.signal?.throwIfAborted();
|
|
223
|
+
await pause(500, options.signal);
|
|
224
|
+
return null;
|
|
225
|
+
});
|
|
226
|
+
clearTimeout(connectTimeout);
|
|
227
|
+
if (!response)
|
|
228
|
+
continue;
|
|
229
|
+
if (!response.ok || !response.body) {
|
|
230
|
+
throw new ConstellationError(response.status, "stream_error", `Event stream returned ${response.status}`);
|
|
231
|
+
}
|
|
232
|
+
const reader = response.body.pipeThrough(new TextDecoderStream).getReader();
|
|
233
|
+
let buffer = "";
|
|
234
|
+
try {
|
|
211
235
|
while (true) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
const { value, done } = await reader.read();
|
|
237
|
+
if (done)
|
|
238
|
+
break;
|
|
239
|
+
buffer += value.replace(/\r\n/g, `
|
|
240
|
+
`);
|
|
241
|
+
let boundary;
|
|
242
|
+
while ((boundary = buffer.indexOf(`
|
|
243
|
+
|
|
244
|
+
`)) >= 0) {
|
|
245
|
+
const frame = buffer.slice(0, boundary);
|
|
246
|
+
buffer = buffer.slice(boundary + 2);
|
|
247
|
+
const data = frame.split(`
|
|
248
|
+
`).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join(`
|
|
249
|
+
`);
|
|
250
|
+
if (!data)
|
|
251
|
+
continue;
|
|
252
|
+
const event = JSON.parse(data);
|
|
253
|
+
if (event.seq <= after)
|
|
254
|
+
continue;
|
|
255
|
+
after = event.seq;
|
|
256
|
+
yield event;
|
|
257
|
+
if (["request.completed", "request.failed", "request.cancelled"].includes(event.type))
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
options.signal?.throwIfAborted();
|
|
263
|
+
} finally {
|
|
264
|
+
await reader.cancel().catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
const inspection = await this.#inspect(id, options.signal);
|
|
267
|
+
if (terminal(inspection.request.status)) {
|
|
268
|
+
const tail = await this.#call(`/v1/requests/${encodeURIComponent(id)}/events?after=${after}`, "GET", undefined, options.signal);
|
|
269
|
+
for (const event of tail.items) {
|
|
270
|
+
if (event.seq <= after)
|
|
271
|
+
continue;
|
|
272
|
+
after = event.seq;
|
|
273
|
+
yield event;
|
|
274
|
+
}
|
|
275
|
+
if (tail.items.length < 500)
|
|
276
|
+
return;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
await pause(500, options.signal);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async#handleInput(id, inputId, options = {}) {
|
|
283
|
+
return this.#processInputs(id, { ...options, targetInputId: inputId });
|
|
284
|
+
}
|
|
285
|
+
async#processInputs(id, options = {}) {
|
|
286
|
+
const consumerId = options.consumerId ?? uuid7();
|
|
287
|
+
while (true) {
|
|
288
|
+
const state = await this.#inspect(id, options.signal);
|
|
289
|
+
options.onUpdate?.(state);
|
|
290
|
+
if (terminal(state.request.status))
|
|
291
|
+
return state;
|
|
292
|
+
if (options.targetInputId) {
|
|
293
|
+
const target = state.inputs.find((input) => input.id === options.targetInputId);
|
|
294
|
+
if (!target)
|
|
295
|
+
throw new Error(`Input ${options.targetInputId} is not in request ${id}`);
|
|
296
|
+
if (target.resolvedAt)
|
|
297
|
+
return state;
|
|
298
|
+
}
|
|
299
|
+
let handled = false;
|
|
300
|
+
for (const pending of state.inputs.filter((i) => !i.resolvedAt && (!options.targetInputId || i.id === options.targetInputId))) {
|
|
301
|
+
if (pending.claimId && pending.claimId !== consumerId && !(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
302
|
+
continue;
|
|
303
|
+
if (pending.kind === "before_model" || pending.kind === "after_model" || pending.kind === "after_step") {
|
|
304
|
+
const name = pending.kind === "before_model" ? "beforeModel" : pending.kind === "after_model" ? "afterModel" : "afterStep";
|
|
305
|
+
const handler = options.checkpoints?.[name];
|
|
306
|
+
if (!handler)
|
|
307
|
+
continue;
|
|
308
|
+
try {
|
|
309
|
+
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
312
|
+
continue;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
316
|
+
const span = await this.#startSpan(id, `caller.${name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
317
|
+
let closed = false;
|
|
235
318
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.map((line) => line.slice(5).trimStart()).join("\n");
|
|
247
|
-
if (!data)
|
|
248
|
-
continue;
|
|
249
|
-
const event = JSON.parse(data);
|
|
250
|
-
if (event.seq <= after)
|
|
251
|
-
continue;
|
|
252
|
-
after = event.seq;
|
|
253
|
-
yield event;
|
|
254
|
-
if (["request.completed", "request.failed", "request.cancelled"].includes(event.type))
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
options.signal?.throwIfAborted();
|
|
261
|
-
// A broken live connection resumes from the last delivered sequence.
|
|
262
|
-
}
|
|
263
|
-
finally {
|
|
264
|
-
await reader.cancel().catch(() => { });
|
|
319
|
+
const signal = activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
320
|
+
const response = await bounded(handler(pending.payload, state, { inputId: pending.id, parentSpanId: span.id, signal }), signal);
|
|
321
|
+
await span.end({}, activeSignal);
|
|
322
|
+
closed = true;
|
|
323
|
+
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
324
|
+
await this.#respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!activeSignal?.aborted && !closed)
|
|
327
|
+
await span.end({ status: "error", attributes: { error: callerErrorDetails(error) } }, activeSignal).catch(() => {});
|
|
328
|
+
throw error;
|
|
265
329
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
for (const event of tail.items) {
|
|
270
|
-
if (event.seq <= after)
|
|
271
|
-
continue;
|
|
272
|
-
after = event.seq;
|
|
273
|
-
yield event;
|
|
274
|
-
}
|
|
275
|
-
if (tail.items.length < 500)
|
|
276
|
-
return;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
await pause(500, options.signal);
|
|
330
|
+
}, options.signal);
|
|
331
|
+
handled = true;
|
|
332
|
+
continue;
|
|
280
333
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
options.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (target.resolvedAt)
|
|
299
|
-
return state;
|
|
300
|
-
}
|
|
301
|
-
let handled = false;
|
|
302
|
-
for (const pending of state.inputs.filter((i) => !i.resolvedAt && (!options.targetInputId || i.id === options.targetInputId))) {
|
|
303
|
-
if (pending.claimId && pending.claimId !== consumerId &&
|
|
304
|
-
!(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
305
|
-
continue;
|
|
306
|
-
if (pending.kind === "before_model" || pending.kind === "after_model" || pending.kind === "after_step") {
|
|
307
|
-
const name = pending.kind === "before_model" ? "beforeModel" : pending.kind === "after_model" ? "afterModel" : "afterStep";
|
|
308
|
-
const handler = options.checkpoints?.[name];
|
|
309
|
-
if (!handler)
|
|
310
|
-
continue;
|
|
311
|
-
try {
|
|
312
|
-
await this.claim(id, pending.id, consumerId, options.signal);
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
316
|
-
continue;
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
320
|
-
const span = await this.startSpan(id, `caller.${name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
321
|
-
let closed = false;
|
|
322
|
-
try {
|
|
323
|
-
const signal = activeSignal
|
|
324
|
-
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
325
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
326
|
-
const response = await bounded(handler(pending.payload, state, { inputId: pending.id, parentSpanId: span.id, signal }), signal);
|
|
327
|
-
await span.end({}, activeSignal);
|
|
328
|
-
closed = true;
|
|
329
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
330
|
-
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
if (!activeSignal?.aborted && !closed)
|
|
334
|
-
await span.end({ status: "error" }, activeSignal).catch(() => { });
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
}, options.signal);
|
|
338
|
-
handled = true;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
const userInput = options.userInput;
|
|
342
|
-
if (pending.kind === "user_input" && userInput) {
|
|
343
|
-
try {
|
|
344
|
-
await this.claim(id, pending.id, consumerId, options.signal);
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
348
|
-
continue;
|
|
349
|
-
throw error;
|
|
350
|
-
}
|
|
351
|
-
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
352
|
-
const signal = activeSignal
|
|
353
|
-
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
354
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
355
|
-
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
356
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
357
|
-
await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
358
|
-
}, options.signal);
|
|
359
|
-
handled = true;
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (pending.kind !== "tool")
|
|
363
|
-
continue;
|
|
364
|
-
const tool = options.tools?.[pending.payload.name ?? ""];
|
|
365
|
-
if (!tool && !pending.payload.validationError)
|
|
366
|
-
continue;
|
|
367
|
-
try {
|
|
368
|
-
await this.claim(id, pending.id, consumerId, options.signal);
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
372
|
-
continue;
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
376
|
-
const span = await this.startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
377
|
-
let result;
|
|
378
|
-
let isError = false;
|
|
379
|
-
try {
|
|
380
|
-
if (pending.payload.validationError)
|
|
381
|
-
throw new Error(pending.payload.validationError);
|
|
382
|
-
if (!tool)
|
|
383
|
-
throw new Error("Tool unavailable");
|
|
384
|
-
const toolSignal = activeSignal
|
|
385
|
-
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
386
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
387
|
-
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
388
|
-
inputId: pending.id,
|
|
389
|
-
requestId: id,
|
|
390
|
-
signal: toolSignal,
|
|
391
|
-
}), toolSignal);
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
activeSignal?.throwIfAborted();
|
|
395
|
-
isError = true;
|
|
396
|
-
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
397
|
-
}
|
|
398
|
-
await span.end({ status: isError ? "error" : "ok" }, activeSignal);
|
|
399
|
-
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
400
|
-
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
401
|
-
}, options.signal);
|
|
402
|
-
handled = true;
|
|
403
|
-
}
|
|
404
|
-
if (options.targetInputId) {
|
|
405
|
-
if (!handled)
|
|
406
|
-
throw new Error(`Input ${options.targetInputId} could not be handled`);
|
|
407
|
-
return this.inspect(id, options.signal);
|
|
408
|
-
}
|
|
409
|
-
await pause(500, options.signal);
|
|
334
|
+
const userInput = options.userInput;
|
|
335
|
+
if (pending.kind === "user_input" && userInput) {
|
|
336
|
+
try {
|
|
337
|
+
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
340
|
+
continue;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
344
|
+
const signal = activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
345
|
+
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
346
|
+
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
347
|
+
await this.#respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
348
|
+
}, options.signal);
|
|
349
|
+
handled = true;
|
|
350
|
+
continue;
|
|
410
351
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
352
|
+
if (pending.kind !== "tool")
|
|
353
|
+
continue;
|
|
354
|
+
const tool = options.tools?.[pending.payload.name ?? ""];
|
|
355
|
+
if (!tool && !pending.payload.validationError)
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
361
|
+
continue;
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
365
|
+
const span = await this.#startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
366
|
+
let result;
|
|
367
|
+
let isError = false;
|
|
368
|
+
let errorDetails;
|
|
369
|
+
try {
|
|
370
|
+
if (pending.payload.validationError)
|
|
371
|
+
throw new Error(pending.payload.validationError);
|
|
372
|
+
if (!tool)
|
|
373
|
+
throw new Error("Tool unavailable");
|
|
374
|
+
const toolSignal = activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
375
|
+
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
376
|
+
inputId: pending.id,
|
|
377
|
+
requestId: id,
|
|
378
|
+
signal: toolSignal
|
|
379
|
+
}), toolSignal);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
activeSignal?.throwIfAborted();
|
|
382
|
+
isError = true;
|
|
383
|
+
errorDetails = callerErrorDetails(error);
|
|
384
|
+
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
385
|
+
}
|
|
386
|
+
await span.end({ status: isError ? "error" : "ok", ...errorDetails ? { attributes: { error: errorDetails } } : {} }, activeSignal);
|
|
387
|
+
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
388
|
+
await this.#respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
389
|
+
}, options.signal);
|
|
390
|
+
handled = true;
|
|
391
|
+
}
|
|
392
|
+
if (options.targetInputId) {
|
|
393
|
+
if (!handled)
|
|
394
|
+
throw new Error(`Input ${options.targetInputId} could not be handled`);
|
|
395
|
+
return this.#inspect(id, options.signal);
|
|
396
|
+
}
|
|
397
|
+
await pause(500, options.signal);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
get models() {
|
|
401
|
+
return {
|
|
402
|
+
list: (scope, signal) => this.#call(`/v1/models${queryString(scope)}`, "GET", undefined, signal),
|
|
403
|
+
resolve: (input, signal) => this.#call("/v1/registry/resolve", "POST", input, signal)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
get requests() {
|
|
407
|
+
return {
|
|
408
|
+
create: (input, signal) => this.#submit(input, signal),
|
|
409
|
+
preview: (input, signal) => this.#resolve(input, signal),
|
|
410
|
+
get: (id, signal) => this.#inspect(id, signal),
|
|
411
|
+
getSummary: (id, signal) => this.#inspectSummary(id, signal),
|
|
412
|
+
getTimings: (id, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/timings`, "GET", undefined, signal),
|
|
413
|
+
list: (query = {}, signal) => this.#list(query, signal),
|
|
414
|
+
cancel: (id, signal) => this.#cancel(id, signal),
|
|
415
|
+
listEvents: (id, after = 0, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/events${queryString({ after })}`, "GET", undefined, signal),
|
|
416
|
+
streamEvents: (id, options = {}) => this.#events(id, options),
|
|
417
|
+
listRecords: (id, kind, query = {}, signal) => this.#records(id, kind, query, signal),
|
|
418
|
+
getTimeline: (id, options = {}) => this.#timeline(id, options),
|
|
419
|
+
getHttpExchange: (id, attemptId, query = {}, signal) => {
|
|
420
|
+
return this.#call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}${queryString(query)}`, "GET", undefined, signal);
|
|
421
|
+
},
|
|
422
|
+
getHttpBody: (id, attemptId, query, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
get inputs() {
|
|
426
|
+
return {
|
|
427
|
+
handle: (requestId, inputId, handlers) => this.#handleInput(requestId, inputId, handlers),
|
|
428
|
+
poll: (requestId, handlers) => this.#processInputs(requestId, handlers),
|
|
429
|
+
claim: (requestId, inputId, claimId, signal) => this.#claim(requestId, inputId, claimId, signal),
|
|
430
|
+
respond: (requestId, inputId, response, signal) => this.#respond(requestId, inputId, response, signal)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
get spans() {
|
|
434
|
+
return {
|
|
435
|
+
start: (requestId, name, options = {}, signal) => this.#startSpan(requestId, name, options, signal),
|
|
436
|
+
add: (requestId, spans, signal) => this.#addSpans(requestId, spans, signal)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
get logs() {
|
|
440
|
+
return { add: (requestId, logs, signal) => this.#addLogs(requestId, logs, signal) };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/index.ts
|
|
445
|
+
function queryString2(values) {
|
|
446
|
+
const params = new URLSearchParams;
|
|
447
|
+
for (const [key, value] of Object.entries(values))
|
|
448
|
+
if (value !== undefined && value !== null)
|
|
449
|
+
params.set(key, String(value));
|
|
450
|
+
return params.size ? `?${params}` : "";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
class Constellation extends RequestClient {
|
|
454
|
+
constructor(options) {
|
|
455
|
+
if (!options.apiKey)
|
|
456
|
+
throw new Error("A bearer API key is required");
|
|
457
|
+
const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
458
|
+
const authenticatedFetch = (input, init) => fetcher(input, {
|
|
459
|
+
...init,
|
|
460
|
+
headers: { ...init?.headers, Authorization: `Bearer ${options.apiKey}` },
|
|
461
|
+
credentials: "omit"
|
|
462
|
+
});
|
|
463
|
+
super({
|
|
464
|
+
baseUrl: options.baseUrl,
|
|
465
|
+
timeoutMs: options.timeoutMs,
|
|
466
|
+
fetch: authenticatedFetch
|
|
467
|
+
});
|
|
468
|
+
this.#baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
469
|
+
this.#fetch = authenticatedFetch;
|
|
470
|
+
this.#timeoutMs = options.timeoutMs ?? 30000;
|
|
471
|
+
}
|
|
472
|
+
#baseUrl;
|
|
473
|
+
#fetch;
|
|
474
|
+
#timeoutMs;
|
|
475
|
+
async#call(path, method = "GET", body, signal) {
|
|
476
|
+
const response = await this.#fetch(`${this.#baseUrl}${path}`, {
|
|
477
|
+
method,
|
|
478
|
+
headers: body === undefined ? {} : { "Content-Type": "application/json" },
|
|
479
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
480
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(this.#timeoutMs)]) : AbortSignal.timeout(this.#timeoutMs)
|
|
481
|
+
});
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
const detail = await response.json().catch(() => ({}));
|
|
484
|
+
throw new ConstellationError(response.status, detail.error?.code ?? "http_error", detail.error?.message ?? `Constellation returned ${response.status}`);
|
|
485
|
+
}
|
|
486
|
+
return response.status === 204 ? undefined : response.json();
|
|
487
|
+
}
|
|
488
|
+
discover(signal) {
|
|
489
|
+
return this.#call("/v1/discover", "GET", undefined, signal);
|
|
490
|
+
}
|
|
491
|
+
get management() {
|
|
492
|
+
return {
|
|
493
|
+
registry: {
|
|
494
|
+
get: (signal) => this.#call("/v1/management/registry", "GET", undefined, signal),
|
|
495
|
+
listRows: (kind, query = {}, signal) => this.#call(`/v1/management/registry/list/${kind}${queryString2(query)}`, "GET", undefined, signal),
|
|
496
|
+
update: (input, signal) => this.#call("/v1/management/registry", "PUT", input, signal),
|
|
497
|
+
applyChanges: (changes, signal) => this.#call("/v1/management/registry/changes", "POST", { changes }, signal),
|
|
498
|
+
listHistory: (kind, id, query = {}, signal) => this.#call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${queryString2(query)}`, "GET", undefined, signal),
|
|
499
|
+
rollback: (kind, id, input, signal) => this.#call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/rollback`, "POST", input, signal),
|
|
500
|
+
preview: (input, signal) => this.#call("/v1/management/registry/preview", "POST", input, signal),
|
|
501
|
+
testRules: (scope, signal) => this.#call("/v1/management/registry/rules/test", "POST", scope, signal)
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
get telemetry() {
|
|
506
|
+
const requestPath = (id) => `/v1/telemetry/requests/${encodeURIComponent(id)}`;
|
|
507
|
+
const read = (path, options = {}, extra = {}) => {
|
|
508
|
+
const value = options;
|
|
509
|
+
return this.#call(`${path}${queryString2({ ...extra, include: value.include })}`, "GET", undefined, value.signal);
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
requests: {
|
|
513
|
+
list: (query = {}, signal) => this.#call(`/v1/telemetry/requests${queryString2(query)}`, "GET", undefined, signal),
|
|
514
|
+
get: (id, options = {}) => read(requestPath(id), options),
|
|
515
|
+
getSummary: (id, options = {}) => read(requestPath(id), options, { summary: "true" }),
|
|
516
|
+
getTimings: (id, signal) => this.#call(`${requestPath(id)}/timings`, "GET", undefined, signal),
|
|
517
|
+
listEvents: (id, after = 0, options = {}) => read(`${requestPath(id)}/events`, options, { after }),
|
|
518
|
+
listRecords: (id, kind, query = {}, signal) => this.#call(`${requestPath(id)}/records/${kind}${queryString2(query)}`, "GET", undefined, signal),
|
|
519
|
+
getTimeline: (id, options = {}) => read(`${requestPath(id)}/timeline`, options, { requestId: options.requestId }),
|
|
520
|
+
getHttpExchange: (id, attemptId, options = {}) => {
|
|
521
|
+
const value = options;
|
|
522
|
+
return read(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}`, value, { offset: value.offset, limit: value.limit });
|
|
523
|
+
},
|
|
524
|
+
getHttpBody: (id, attemptId, query, signal) => this.#call(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}/body${queryString2(query)}`, "GET", undefined, signal)
|
|
525
|
+
},
|
|
526
|
+
system: {
|
|
527
|
+
get: (options = {}) => read("/v1/telemetry/system", options),
|
|
528
|
+
listHistory: (query = {}, signal) => this.#call(`/v1/telemetry/system/history${queryString2(query)}`, "GET", undefined, signal),
|
|
529
|
+
getComponent: (component, options = {}) => {
|
|
530
|
+
const value = options;
|
|
531
|
+
return read(`/v1/telemetry/system/${encodeURIComponent(component)}`, value, { offset: value.offset, status: value.status, statusNot: value.statusNot, q: value.q });
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
observability: {
|
|
535
|
+
get: (query = {}, signal) => this.#call(`/v1/telemetry/observability${queryString2(query)}`, "GET", undefined, signal),
|
|
536
|
+
getSeries: (query, signal) => this.#call(`/v1/telemetry/observability/series${queryString2(query)}`, "GET", undefined, signal),
|
|
537
|
+
getTiming: (query = {}, signal) => this.#call(`/v1/telemetry/observability/timing${queryString2(query)}`, "GET", undefined, signal),
|
|
538
|
+
getTraffic: (query = {}, signal) => this.#call(`/v1/telemetry/observability/traffic${queryString2(query)}`, "GET", undefined, signal),
|
|
539
|
+
getBilling: (query = {}, signal) => this.#call(`/v1/telemetry/observability/billing${queryString2(query)}`, "GET", undefined, signal)
|
|
540
|
+
},
|
|
541
|
+
jobs: {
|
|
542
|
+
listHistory: (query = {}, signal) => this.#call(`/v1/telemetry/jobs/history${queryString2(query)}`, "GET", undefined, signal)
|
|
543
|
+
},
|
|
544
|
+
notifications: {
|
|
545
|
+
list: (query = {}, signal) => this.#call(`/v1/telemetry/notifications${queryString2(query)}`, "GET", undefined, signal),
|
|
546
|
+
get: (id, query = {}, signal) => this.#call(`/v1/telemetry/notifications/${encodeURIComponent(id)}${queryString2(query)}`, "GET", undefined, signal),
|
|
547
|
+
listEvents: (query = {}, signal) => this.#call(`/v1/telemetry/notifications/events${queryString2(query)}`, "GET", undefined, signal)
|
|
548
|
+
},
|
|
549
|
+
logs: {
|
|
550
|
+
searchSystem: (query = {}, signal) => this.#call(`/v1/telemetry/logs/system${queryString2(query)}`, "GET", undefined, signal),
|
|
551
|
+
searchRequests: (query = {}, signal) => this.#call(`/v1/telemetry/logs/requests${queryString2(query)}`, "GET", undefined, signal),
|
|
552
|
+
searchAudit: (query = {}, signal) => this.#call(`/v1/telemetry/logs/audit${queryString2(query)}`, "GET", undefined, signal)
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
get webhooks() {
|
|
557
|
+
return {
|
|
558
|
+
verify: async (request, expectedToken) => {
|
|
559
|
+
if (!expectedToken)
|
|
560
|
+
throw new Error("Constellation webhook token is not configured");
|
|
561
|
+
const actual = request.headers.get("x-constellation-webhook-token") ?? "";
|
|
562
|
+
let mismatch = actual.length ^ expectedToken.length;
|
|
563
|
+
for (let i = 0;i < Math.max(actual.length, expectedToken.length); i++)
|
|
564
|
+
mismatch |= (actual.charCodeAt(i) || 0) ^ (expectedToken.charCodeAt(i) || 0);
|
|
565
|
+
if (mismatch !== 0)
|
|
566
|
+
throw new Error("Invalid Constellation webhook token");
|
|
567
|
+
const body = await request.json();
|
|
568
|
+
if (!body || typeof body !== "object")
|
|
569
|
+
throw new Error("Invalid Constellation webhook body");
|
|
570
|
+
const event = body;
|
|
571
|
+
if (typeof event.requestId !== "string" || !event.requestId)
|
|
572
|
+
throw new Error("Invalid Constellation request ID");
|
|
573
|
+
if (typeof event.inputId === "string" && ["tool", "before_model", "after_model", "after_step", "user_input"].includes(String(event.kind))) {
|
|
574
|
+
if (request.headers.get("x-constellation-input-id") !== event.inputId || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload))
|
|
575
|
+
throw new Error("Invalid Constellation input notification");
|
|
576
|
+
return event;
|
|
577
|
+
}
|
|
578
|
+
if (event.kind === "request.events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
|
|
579
|
+
return event;
|
|
580
|
+
if (["request.completed", "request.failed", "request.cancelled"].includes(String(event.kind)) && Number.isSafeInteger(event.eventSeq))
|
|
581
|
+
return event;
|
|
582
|
+
throw new Error("Unknown Constellation webhook event");
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
563
586
|
}
|
|
587
|
+
export {
|
|
588
|
+
uuid7,
|
|
589
|
+
ConstellationError,
|
|
590
|
+
Constellation
|
|
591
|
+
};
|