@lionhummer/constellation 0.6.0 → 0.9.1
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 +327 -160
- package/dist/index.js +443 -281
- package/dist/types/client-core/src/agent.d.ts +81 -0
- package/dist/types/client-core/src/index.d.ts +102 -131
- package/dist/types/client-core/src/input-receiver.d.ts +24 -0
- package/dist/types/sdk/src/index.d.ts +5 -7
- package/examples/quickstart.mjs +3 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
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 } : {} };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
1
|
// ../client-core/src/index.ts
|
|
20
2
|
class ConstellationError extends Error {
|
|
21
3
|
status;
|
|
@@ -27,6 +9,66 @@ class ConstellationError extends Error {
|
|
|
27
9
|
this.name = "ConstellationError";
|
|
28
10
|
}
|
|
29
11
|
}
|
|
12
|
+
function observationBytes(observations) {
|
|
13
|
+
const spans = (observations.spans ?? []).map((s) => ({ ...s, stepId: s.stepId ?? null, attemptId: s.attemptId ?? null, parentSpanId: s.parentSpanId ?? null, endedAt: s.endedAt ?? null, attributes: s.attributes ?? null }));
|
|
14
|
+
const logs = (observations.logs ?? []).map((l) => ({ ...l, stepId: l.stepId ?? null, spanId: l.spanId ?? null, attributes: l.attributes ?? null }));
|
|
15
|
+
return new TextEncoder().encode(JSON.stringify({ spans, logs })).byteLength;
|
|
16
|
+
}
|
|
17
|
+
var TIMESTAMP = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$/i;
|
|
18
|
+
function normalizeTimestamp(value) {
|
|
19
|
+
const match = TIMESTAMP.exec(value);
|
|
20
|
+
if (!match)
|
|
21
|
+
return value;
|
|
22
|
+
const millis = Date.parse(`${match[1]}.${(match[2] ?? "").padEnd(3, "0").slice(0, 3)}${match[3].toUpperCase()}`);
|
|
23
|
+
if (!Number.isFinite(millis))
|
|
24
|
+
return value;
|
|
25
|
+
const normalized = new Date(millis).toISOString();
|
|
26
|
+
return /^\d{4}-/.test(normalized) ? normalized : value;
|
|
27
|
+
}
|
|
28
|
+
function normalizeObservations(observations) {
|
|
29
|
+
return {
|
|
30
|
+
...observations,
|
|
31
|
+
...observations.spans ? { spans: observations.spans.map((span) => ({
|
|
32
|
+
...span,
|
|
33
|
+
startedAt: normalizeTimestamp(span.startedAt),
|
|
34
|
+
...span.endedAt ? { endedAt: normalizeTimestamp(span.endedAt) } : {}
|
|
35
|
+
})) } : {},
|
|
36
|
+
...observations.logs ? { logs: observations.logs.map((log) => ({ ...log, timestamp: normalizeTimestamp(log.timestamp) })) } : {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function validateObservations(observations) {
|
|
40
|
+
const spans = observations.spans ?? [], logs = observations.logs ?? [];
|
|
41
|
+
const ids = new Set;
|
|
42
|
+
const uuid = (id) => !id || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) && id !== "00000000-0000-0000-0000-000000000000";
|
|
43
|
+
const attributes = (value) => value == null || typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
const invalid = () => {
|
|
45
|
+
throw new ConstellationError(400, "invalid_telemetry", "Telemetry exceeds limits or contains invalid observations");
|
|
46
|
+
};
|
|
47
|
+
if (spans.length > 512 || logs.length > 128 || observationBytes(observations) > 1024 * 1024)
|
|
48
|
+
invalid();
|
|
49
|
+
for (const span of spans) {
|
|
50
|
+
const start = Date.parse(span.startedAt), end = span.endedAt ? Date.parse(span.endedAt) : NaN;
|
|
51
|
+
if (!span.id || !uuid(span.id) || !uuid(span.stepId) || !uuid(span.attemptId) || !uuid(span.parentSpanId) || !attributes(span.attributes) || ids.has(span.id) || !span.name.trim() || new TextEncoder().encode(span.name).byteLength > 256 || !Number.isFinite(start) || !Number.isFinite(end) || end < start || !["ok", "error", "cancelled"].includes(span.status) || span.parentSpanId === span.id)
|
|
52
|
+
invalid();
|
|
53
|
+
ids.add(span.id);
|
|
54
|
+
}
|
|
55
|
+
const parents = new Map(spans.map((span) => [span.id, span.parentSpanId]));
|
|
56
|
+
for (const span of spans) {
|
|
57
|
+
const seen = new Set;
|
|
58
|
+
let id = span.id;
|
|
59
|
+
while (id) {
|
|
60
|
+
if (seen.has(id))
|
|
61
|
+
invalid();
|
|
62
|
+
seen.add(id);
|
|
63
|
+
id = parents.get(id) ?? undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const log of logs) {
|
|
67
|
+
if (!log.id || !uuid(log.id) || !uuid(log.stepId) || !uuid(log.spanId) || !attributes(log.attributes) || ids.has(log.id) || !log.message || new TextEncoder().encode(log.message).byteLength > 8192 || !["debug", "info", "warn", "error"].includes(log.level) || !Number.isFinite(Date.parse(log.timestamp)))
|
|
68
|
+
invalid();
|
|
69
|
+
ids.add(log.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
30
72
|
function uuid7() {
|
|
31
73
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
32
74
|
let ms = Date.now();
|
|
@@ -39,7 +81,6 @@ function uuid7() {
|
|
|
39
81
|
const h = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
40
82
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
41
83
|
}
|
|
42
|
-
var terminal = (s) => ["completed", "failed", "cancelled"].includes(s);
|
|
43
84
|
function queryString(values) {
|
|
44
85
|
const params = new URLSearchParams;
|
|
45
86
|
for (const [key, value] of Object.entries(values))
|
|
@@ -61,13 +102,14 @@ async function pause(ms, signal) {
|
|
|
61
102
|
signal?.addEventListener("abort", abort, { once: true });
|
|
62
103
|
});
|
|
63
104
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
var COMPRESS_FROM_BYTES = 16 * 1024;
|
|
106
|
+
async function encodeBody(body, compress) {
|
|
107
|
+
const json = JSON.stringify(body);
|
|
108
|
+
if (!compress || json.length < COMPRESS_FROM_BYTES || typeof CompressionStream === "undefined") {
|
|
109
|
+
return { body: json, headers: { "Content-Type": "application/json" } };
|
|
110
|
+
}
|
|
111
|
+
const gzipped = await new Response(new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();
|
|
112
|
+
return { body: gzipped, headers: { "Content-Type": "application/json", "Content-Encoding": "gzip" } };
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
class RequestClient {
|
|
@@ -82,12 +124,11 @@ class RequestClient {
|
|
|
82
124
|
return `${this.#options.baseUrl.replace(/\/$/, "")}${route}`;
|
|
83
125
|
}
|
|
84
126
|
async#call(path, method = "GET", body, signal) {
|
|
127
|
+
const encoded = body === undefined ? undefined : await encodeBody(body, this.#options.compressRequests ?? true);
|
|
85
128
|
const response = await this.#fetcher(this.#url(path), {
|
|
86
129
|
method,
|
|
87
|
-
headers: {
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
130
|
+
headers: encoded?.headers ?? {},
|
|
131
|
+
body: encoded?.body,
|
|
91
132
|
signal: signal ? AbortSignal.any([
|
|
92
133
|
signal,
|
|
93
134
|
AbortSignal.timeout(this.#options.timeoutMs ?? 30000)
|
|
@@ -99,8 +140,22 @@ class RequestClient {
|
|
|
99
140
|
}
|
|
100
141
|
return response.status === 204 ? undefined : response.json();
|
|
101
142
|
}
|
|
102
|
-
#submit(input, signal) {
|
|
103
|
-
|
|
143
|
+
async#submit(input, signal) {
|
|
144
|
+
const prepared = JSON.parse(JSON.stringify(input));
|
|
145
|
+
if (prepared.telemetry) {
|
|
146
|
+
prepared.telemetry = normalizeObservations(prepared.telemetry);
|
|
147
|
+
validateObservations(prepared.telemetry);
|
|
148
|
+
}
|
|
149
|
+
for (let attempt = 0;; attempt++) {
|
|
150
|
+
try {
|
|
151
|
+
return await this.#call("/v1/requests", "POST", prepared, signal);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
signal?.throwIfAborted();
|
|
154
|
+
if (attempt >= 2 || error instanceof ConstellationError && error.status !== 429 && error.status < 500)
|
|
155
|
+
throw error;
|
|
156
|
+
await pause(100 * 2 ** attempt, signal);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
104
159
|
}
|
|
105
160
|
#inspect(id, signal) {
|
|
106
161
|
return this.#call(`/v1/requests/${encodeURIComponent(id)}`, "GET", undefined, signal);
|
|
@@ -132,80 +187,18 @@ class RequestClient {
|
|
|
132
187
|
params.set(k, String(v));
|
|
133
188
|
return this.#call(`/v1/requests?${params}`, "GET", undefined, signal);
|
|
134
189
|
}
|
|
135
|
-
#cancel(id, signal) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const renewal = (async () => {
|
|
145
|
-
while (!signal.aborted) {
|
|
146
|
-
try {
|
|
147
|
-
await pause(15000, signal);
|
|
148
|
-
} catch {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
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;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
})();
|
|
161
|
-
return {
|
|
162
|
-
signal,
|
|
163
|
-
stop: async () => {
|
|
164
|
-
controller.abort();
|
|
165
|
-
await renewal;
|
|
190
|
+
async#cancel(id, signal) {
|
|
191
|
+
for (let attempt = 0;; attempt++) {
|
|
192
|
+
try {
|
|
193
|
+
return await this.#call(`/v1/requests/${encodeURIComponent(id)}/cancel`, "POST", {}, signal);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
signal?.throwIfAborted();
|
|
196
|
+
if (attempt >= 7 || !(error instanceof ConstellationError) || error.status !== 409 || error.code !== "conflict")
|
|
197
|
+
throw error;
|
|
198
|
+
await pause(Math.min(25 * 2 ** attempt, 200), signal);
|
|
166
199
|
}
|
|
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
200
|
}
|
|
176
201
|
}
|
|
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
|
|
204
|
-
};
|
|
205
|
-
return this.#addSpans(id, [closed, ...options2.spans ?? []], signal2);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
202
|
async* #events(id, options = {}) {
|
|
210
203
|
let after = options.after ?? 0;
|
|
211
204
|
while (true) {
|
|
@@ -227,6 +220,11 @@ class RequestClient {
|
|
|
227
220
|
if (!response)
|
|
228
221
|
continue;
|
|
229
222
|
if (!response.ok || !response.body) {
|
|
223
|
+
if (response.status === 429 || response.status >= 500) {
|
|
224
|
+
await response.body?.cancel();
|
|
225
|
+
await pause(500, options.signal);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
230
228
|
throw new ConstellationError(response.status, "stream_error", `Event stream returned ${response.status}`);
|
|
231
229
|
}
|
|
232
230
|
const reader = response.body.pipeThrough(new TextDecoderStream).getReader();
|
|
@@ -250,12 +248,10 @@ class RequestClient {
|
|
|
250
248
|
if (!data)
|
|
251
249
|
continue;
|
|
252
250
|
const event = JSON.parse(data);
|
|
253
|
-
if (event.
|
|
251
|
+
if (event.sequence <= after)
|
|
254
252
|
continue;
|
|
255
|
-
after = event.
|
|
253
|
+
after = event.sequence;
|
|
256
254
|
yield event;
|
|
257
|
-
if (["request.completed", "request.failed", "request.cancelled"].includes(event.type))
|
|
258
|
-
return;
|
|
259
255
|
}
|
|
260
256
|
}
|
|
261
257
|
} catch {
|
|
@@ -263,166 +259,39 @@ class RequestClient {
|
|
|
263
259
|
} finally {
|
|
264
260
|
await reader.cancel().catch(() => {});
|
|
265
261
|
}
|
|
266
|
-
const inspection = await this.#
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
const inspection = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/state`, options.signal);
|
|
263
|
+
if (["completed", "failed", "cancelled"].includes(inspection.request.status)) {
|
|
264
|
+
while (true) {
|
|
265
|
+
const before = after;
|
|
266
|
+
const tail = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/events?after=${after}`, options.signal);
|
|
267
|
+
for (const event of tail.items) {
|
|
268
|
+
if (event.sequence <= after)
|
|
269
|
+
continue;
|
|
270
|
+
after = event.sequence;
|
|
271
|
+
yield event;
|
|
272
|
+
}
|
|
273
|
+
if (tail.items.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
if (after === before)
|
|
276
|
+
throw new Error("Replay page did not advance the event cursor");
|
|
274
277
|
}
|
|
275
|
-
if (tail.items.length < 500)
|
|
276
|
-
return;
|
|
277
|
-
continue;
|
|
278
278
|
}
|
|
279
279
|
await pause(500, options.signal);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
async#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const state = await this.#inspect(id, options.signal);
|
|
291
|
-
const inspectEnded = new Date().toISOString();
|
|
292
|
-
const inspectMs = performance.now() - inspectClock;
|
|
293
|
-
options.onUpdate?.(state);
|
|
294
|
-
if (terminal(state.request.status))
|
|
295
|
-
return state;
|
|
296
|
-
if (options.targetInputId) {
|
|
297
|
-
const target = state.inputs.find((input) => input.id === options.targetInputId);
|
|
298
|
-
if (!target)
|
|
299
|
-
throw new Error(`Input ${options.targetInputId} is not in request ${id}`);
|
|
300
|
-
if (target.resolvedAt)
|
|
301
|
-
return state;
|
|
302
|
-
}
|
|
303
|
-
let handled = false;
|
|
304
|
-
for (const pending of state.inputs.filter((i) => !i.resolvedAt && (!options.targetInputId || i.id === options.targetInputId))) {
|
|
305
|
-
if (pending.claimId && pending.claimId !== consumerId && !(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
306
|
-
continue;
|
|
307
|
-
if (pending.kind === "before_model" || pending.kind === "after_model" || pending.kind === "after_step") {
|
|
308
|
-
const name = pending.kind === "before_model" ? "beforeModel" : pending.kind === "after_model" ? "afterModel" : "afterStep";
|
|
309
|
-
const handler = options.checkpoints?.[name];
|
|
310
|
-
if (!handler)
|
|
311
|
-
continue;
|
|
312
|
-
try {
|
|
313
|
-
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
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 ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
324
|
-
const response = await bounded(handler(pending.payload, state, { inputId: pending.id, parentSpanId: span.id, signal }), signal);
|
|
325
|
-
await span.end({}, activeSignal);
|
|
326
|
-
closed = true;
|
|
327
|
-
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
328
|
-
await this.#respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
if (!activeSignal?.aborted && !closed)
|
|
331
|
-
await span.end({ status: "error", attributes: { error: callerErrorDetails(error) } }, activeSignal).catch(() => {});
|
|
332
|
-
throw error;
|
|
333
|
-
}
|
|
334
|
-
}, options.signal);
|
|
335
|
-
handled = true;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
const userInput = options.userInput;
|
|
339
|
-
if (pending.kind === "user_input" && userInput) {
|
|
340
|
-
try {
|
|
341
|
-
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
342
|
-
} catch (error) {
|
|
343
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
344
|
-
continue;
|
|
345
|
-
throw error;
|
|
346
|
-
}
|
|
347
|
-
await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
348
|
-
const signal = activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
349
|
-
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
350
|
-
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
351
|
-
await this.#respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
352
|
-
}, options.signal);
|
|
353
|
-
handled = true;
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (pending.kind !== "tool")
|
|
357
|
-
continue;
|
|
358
|
-
const claimStarted = new Date().toISOString();
|
|
359
|
-
const claimClock = performance.now();
|
|
360
|
-
const tool = options.tools?.[pending.payload.name ?? ""];
|
|
361
|
-
if (!tool && !pending.payload.validationError)
|
|
362
|
-
continue;
|
|
363
|
-
try {
|
|
364
|
-
await this.#claim(id, pending.id, consumerId, options.signal);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (error instanceof ConstellationError && error.status === 409)
|
|
367
|
-
continue;
|
|
282
|
+
async#replayRead(path, signal) {
|
|
283
|
+
for (;; ) {
|
|
284
|
+
signal?.throwIfAborted();
|
|
285
|
+
try {
|
|
286
|
+
return await this.#call(path, "GET", undefined, signal);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
signal?.throwIfAborted();
|
|
289
|
+
if (error instanceof ConstellationError && error.status !== 429 && error.status < 500)
|
|
368
290
|
throw error;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
373
|
-
const span = await this.#startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
374
|
-
const phases = [
|
|
375
|
-
{ id: uuid7(), inputId: pending.id, name: "caller.input.inspect", startedAt: inspectStarted, endedAt: inspectEnded, status: "ok", attributes: { elapsedMs: inspectMs } },
|
|
376
|
-
{ id: uuid7(), inputId: pending.id, name: "caller.input.claim", startedAt: claimStarted, endedAt: claimEnded, status: "ok", attributes: { elapsedMs: claimMs } }
|
|
377
|
-
];
|
|
378
|
-
let result;
|
|
379
|
-
let isError = false;
|
|
380
|
-
let errorDetails;
|
|
381
|
-
try {
|
|
382
|
-
if (pending.payload.validationError)
|
|
383
|
-
throw new Error(pending.payload.validationError);
|
|
384
|
-
if (!tool)
|
|
385
|
-
throw new Error("Tool unavailable");
|
|
386
|
-
const toolSignal = activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)]) : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
387
|
-
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
388
|
-
inputId: pending.id,
|
|
389
|
-
requestId: id,
|
|
390
|
-
parentSpanId: span.id,
|
|
391
|
-
recordSpan: (phase) => phases.push({ ...phase, inputId: pending.id, parentSpanId: span.id }),
|
|
392
|
-
signal: toolSignal
|
|
393
|
-
}), toolSignal);
|
|
394
|
-
} catch (error) {
|
|
395
|
-
activeSignal?.throwIfAborted();
|
|
396
|
-
isError = true;
|
|
397
|
-
errorDetails = callerErrorDetails(error);
|
|
398
|
-
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
399
|
-
}
|
|
400
|
-
await span.end({ status: isError ? "error" : "ok", spans: phases, ...errorDetails ? { attributes: { error: errorDetails } } : {} }, activeSignal);
|
|
401
|
-
await this.#claim(id, pending.id, consumerId, activeSignal);
|
|
402
|
-
const responseStarted = new Date().toISOString();
|
|
403
|
-
const responseClock = performance.now();
|
|
404
|
-
let responseStatus = "ok";
|
|
405
|
-
try {
|
|
406
|
-
await this.#respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
407
|
-
} catch (error) {
|
|
408
|
-
responseStatus = "error";
|
|
409
|
-
throw error;
|
|
410
|
-
} finally {
|
|
411
|
-
const responseSpan = { id: uuid7(), inputId: pending.id, name: "caller.input.respond", startedAt: responseStarted, endedAt: new Date().toISOString(), status: responseStatus, attributes: { elapsedMs: performance.now() - responseClock } };
|
|
412
|
-
await this.#addSpans(id, [responseSpan], activeSignal ? AbortSignal.any([activeSignal, AbortSignal.timeout(2000)]) : AbortSignal.timeout(2000)).catch((error) => {
|
|
413
|
-
if (!(error instanceof ConstellationError && (error.status === 409 || error.status === 404)))
|
|
414
|
-
console.warn("Constellation input-response timing could not be recorded");
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
}, options.signal);
|
|
418
|
-
handled = true;
|
|
419
|
-
}
|
|
420
|
-
if (options.targetInputId) {
|
|
421
|
-
if (!handled)
|
|
422
|
-
throw new Error(`Input ${options.targetInputId} could not be handled`);
|
|
423
|
-
return this.#inspect(id, options.signal);
|
|
291
|
+
if (error instanceof SyntaxError)
|
|
292
|
+
throw error;
|
|
293
|
+
await pause(500, signal);
|
|
424
294
|
}
|
|
425
|
-
await pause(500, options.signal);
|
|
426
295
|
}
|
|
427
296
|
}
|
|
428
297
|
get models() {
|
|
@@ -433,9 +302,10 @@ class RequestClient {
|
|
|
433
302
|
}
|
|
434
303
|
get requests() {
|
|
435
304
|
return {
|
|
436
|
-
|
|
305
|
+
submit: (input, signal) => this.#submit(input, signal),
|
|
437
306
|
preview: (input, signal) => this.#resolve(input, signal),
|
|
438
307
|
get: (id, signal) => this.#inspect(id, signal),
|
|
308
|
+
getState: (id, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/state`, "GET", undefined, signal),
|
|
439
309
|
getSummary: (id, signal) => this.#inspectSummary(id, signal),
|
|
440
310
|
getTimings: (id, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/timings`, "GET", undefined, signal),
|
|
441
311
|
list: (query = {}, signal) => this.#list(query, signal),
|
|
@@ -450,22 +320,306 @@ class RequestClient {
|
|
|
450
320
|
getHttpBody: (id, attemptId, query, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal)
|
|
451
321
|
};
|
|
452
322
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
323
|
+
}
|
|
324
|
+
// ../client-core/src/error-details.ts
|
|
325
|
+
function callerErrorDetails(error) {
|
|
326
|
+
let cause = error;
|
|
327
|
+
const seen = new Set;
|
|
328
|
+
for (let depth = 0;depth < 8 && cause instanceof Error && !seen.has(cause); depth++) {
|
|
329
|
+
seen.add(cause);
|
|
330
|
+
if (!(cause.cause instanceof Error))
|
|
331
|
+
break;
|
|
332
|
+
cause = cause.cause;
|
|
333
|
+
}
|
|
334
|
+
const record = cause && typeof cause === "object" ? cause : {};
|
|
335
|
+
const name = cause instanceof Error ? cause.name.slice(0, 80) : "CallerError";
|
|
336
|
+
const code = typeof record.code === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(record.code) ? record.code : undefined;
|
|
337
|
+
const raw = cause instanceof Error ? cause.message : "Caller handler threw a non-Error value";
|
|
338
|
+
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);
|
|
339
|
+
return { name, message, ...code ? { code } : {} };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ../client-core/src/permanent-codes.json
|
|
343
|
+
var permanent_codes_default = [
|
|
344
|
+
"bad_request",
|
|
345
|
+
"observations_expired",
|
|
346
|
+
"response_conflict"
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
// ../client-core/src/agent.ts
|
|
350
|
+
var PERMANENT_RESPONSE_CODES = new Set(permanent_codes_default);
|
|
351
|
+
function truncateUtf8(value, limit) {
|
|
352
|
+
let result = "", bytes = 0;
|
|
353
|
+
for (const raw of value) {
|
|
354
|
+
const point = raw.codePointAt(0);
|
|
355
|
+
const char = point >= 55296 && point <= 57343 ? "�" : raw;
|
|
356
|
+
const width = new TextEncoder().encode(char).byteLength;
|
|
357
|
+
if (bytes + width > limit)
|
|
358
|
+
break;
|
|
359
|
+
result += char;
|
|
360
|
+
bytes += width;
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
function cleanJson(value) {
|
|
365
|
+
if (typeof value === "string")
|
|
366
|
+
return truncateUtf8(value, Infinity);
|
|
367
|
+
if (Array.isArray(value))
|
|
368
|
+
return value.map(cleanJson);
|
|
369
|
+
if (value && typeof value === "object")
|
|
370
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [truncateUtf8(key, Infinity), cleanJson(item)]));
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class PermanentEventError extends Error {
|
|
375
|
+
status;
|
|
376
|
+
code;
|
|
377
|
+
constructor(status, code, message) {
|
|
378
|
+
super(message);
|
|
379
|
+
this.status = status;
|
|
380
|
+
this.code = code;
|
|
381
|
+
this.name = "PermanentEventError";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
class HookConfigurationError extends PermanentEventError {
|
|
386
|
+
constructor(message) {
|
|
387
|
+
super(400, "invalid_hook_configuration", message);
|
|
388
|
+
this.name = "HookConfigurationError";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
class EventReceiver {
|
|
393
|
+
store;
|
|
394
|
+
constructor(store) {
|
|
395
|
+
this.store = store;
|
|
396
|
+
}
|
|
397
|
+
async receive(event) {
|
|
398
|
+
await this.store.accept(event);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class ClientTelemetry {
|
|
403
|
+
parentSpanId;
|
|
404
|
+
spans = [];
|
|
405
|
+
logs = [];
|
|
406
|
+
constructor(parentSpanId) {
|
|
407
|
+
this.parentSpanId = parentSpanId;
|
|
408
|
+
}
|
|
409
|
+
log(level, message, attributes) {
|
|
410
|
+
this.logs.push({
|
|
411
|
+
id: uuid7(),
|
|
412
|
+
level,
|
|
413
|
+
message,
|
|
414
|
+
timestamp: new Date().toISOString(),
|
|
415
|
+
spanId: this.parentSpanId,
|
|
416
|
+
attributes
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async span(name, attributes, work) {
|
|
420
|
+
const id = uuid7();
|
|
421
|
+
const startedAt = new Date().toISOString();
|
|
422
|
+
const started = performance.now();
|
|
423
|
+
const child = new ClientTelemetry(id);
|
|
424
|
+
let status = "ok";
|
|
425
|
+
let failure;
|
|
426
|
+
try {
|
|
427
|
+
return await work(child);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
status = "error";
|
|
430
|
+
failure = callerErrorDetails(error);
|
|
431
|
+
throw error;
|
|
432
|
+
} finally {
|
|
433
|
+
this.spans.push({
|
|
434
|
+
id,
|
|
435
|
+
name,
|
|
436
|
+
parentSpanId: this.parentSpanId,
|
|
437
|
+
startedAt,
|
|
438
|
+
endedAt: new Date().toISOString(),
|
|
439
|
+
status,
|
|
440
|
+
attributes: {
|
|
441
|
+
...attributes,
|
|
442
|
+
elapsedMs: performance.now() - started,
|
|
443
|
+
...failure ? { error: failure } : {}
|
|
444
|
+
}
|
|
445
|
+
}, ...child.spans);
|
|
446
|
+
this.logs.push(...child.logs);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
snapshot() {
|
|
450
|
+
const spans = structuredClone(this.spans.slice(0, 512));
|
|
451
|
+
const logs = structuredClone(this.logs.slice(0, 128));
|
|
452
|
+
for (const span of spans) {
|
|
453
|
+
span.name = truncateUtf8(span.name, 256).trim() || "(unnamed span)";
|
|
454
|
+
if (span.attributes)
|
|
455
|
+
span.attributes = cleanJson(span.attributes);
|
|
456
|
+
if (span.endedAt && span.endedAt < span.startedAt)
|
|
457
|
+
span.endedAt = span.startedAt;
|
|
458
|
+
}
|
|
459
|
+
for (const log of logs) {
|
|
460
|
+
log.message = truncateUtf8(log.message, 8192);
|
|
461
|
+
if (log.attributes)
|
|
462
|
+
log.attributes = cleanJson(log.attributes);
|
|
463
|
+
if (!log.message)
|
|
464
|
+
log.message = "(empty log)";
|
|
465
|
+
}
|
|
466
|
+
const bytes = () => observationBytes({ spans, logs });
|
|
467
|
+
if (bytes() > 1024 * 1024) {
|
|
468
|
+
for (const item of [...spans, ...logs]) {
|
|
469
|
+
if (new TextEncoder().encode(JSON.stringify(item.attributes ?? {})).byteLength > 4096)
|
|
470
|
+
item.attributes = { omitted: true, reason: "telemetry_byte_limit" };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
while (bytes() > 1024 * 1024 && (logs.length || spans.length > 1)) {
|
|
474
|
+
if (logs.length)
|
|
475
|
+
logs.pop();
|
|
476
|
+
else
|
|
477
|
+
spans.pop();
|
|
478
|
+
}
|
|
479
|
+
const ids = new Set(spans.map((span) => span.id));
|
|
480
|
+
for (const log of logs)
|
|
481
|
+
if (log.spanId && !ids.has(log.spanId))
|
|
482
|
+
delete log.spanId;
|
|
483
|
+
if (spans.length && (spans.length < this.spans.length || logs.length < this.logs.length)) {
|
|
484
|
+
spans[0].attributes = { ...spans[0].attributes, omittedSpans: this.spans.length - spans.length, omittedLogs: this.logs.length - logs.length };
|
|
485
|
+
}
|
|
486
|
+
while (bytes() > 1024 * 1024) {
|
|
487
|
+
if (logs.length)
|
|
488
|
+
logs.pop();
|
|
489
|
+
else if (spans.length > 1)
|
|
490
|
+
spans.pop();
|
|
491
|
+
else if (spans.length)
|
|
492
|
+
spans[0].attributes = { omitted: true, reason: "telemetry_byte_limit" };
|
|
493
|
+
else
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
return { spans, logs };
|
|
460
497
|
}
|
|
461
|
-
|
|
498
|
+
}
|
|
499
|
+
async function prepareResponseBody(event, hooks, telemetry, signal) {
|
|
500
|
+
const context = { event, signal, telemetry };
|
|
501
|
+
const observe = () => ({
|
|
502
|
+
action: "observe",
|
|
503
|
+
idempotencyKey: event.id,
|
|
504
|
+
inReplyTo: { requestId: event.requestId, eventId: event.id },
|
|
505
|
+
telemetry: telemetry.snapshot()
|
|
506
|
+
});
|
|
507
|
+
signal?.throwIfAborted();
|
|
508
|
+
if (event.type !== "model.completed") {
|
|
509
|
+
await telemetry.span("client.model_error", { type: event.type }, async (t) => {
|
|
510
|
+
await hooks.onError?.(event.data, { ...context, telemetry: t });
|
|
511
|
+
});
|
|
512
|
+
return observe();
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const calls = event.data.message.toolCalls ?? [];
|
|
516
|
+
const ids = new Set(calls.map((c) => c.id));
|
|
517
|
+
if (ids.size !== calls.length)
|
|
518
|
+
throw new Error("Duplicate tool-call IDs in model result");
|
|
519
|
+
const concurrency = hooks.toolConcurrency ?? 8;
|
|
520
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 128)
|
|
521
|
+
throw new HookConfigurationError("toolConcurrency must be between 1 and 128");
|
|
522
|
+
const results = new Array(calls.length);
|
|
523
|
+
let next = 0;
|
|
524
|
+
const executions = await Promise.allSettled(Array.from({ length: Math.min(concurrency, calls.length) }, async () => {
|
|
525
|
+
for (;; ) {
|
|
526
|
+
const index = next++;
|
|
527
|
+
if (index >= calls.length)
|
|
528
|
+
return;
|
|
529
|
+
signal?.throwIfAborted();
|
|
530
|
+
const call = calls[index];
|
|
531
|
+
const handler = hooks.tools?.[call.name];
|
|
532
|
+
if (!handler)
|
|
533
|
+
throw new HookConfigurationError(`No handler for tool ${call.name}`);
|
|
534
|
+
const result = await telemetry.span(`tool.${call.name}`, { toolCallId: call.id }, (t) => handler(call.arguments, {
|
|
535
|
+
...context,
|
|
536
|
+
telemetry: t,
|
|
537
|
+
toolCallId: call.id,
|
|
538
|
+
effectId: JSON.stringify([event.id, call.id])
|
|
539
|
+
}));
|
|
540
|
+
results[index] = { id: call.id, result };
|
|
541
|
+
}
|
|
542
|
+
}));
|
|
543
|
+
const failed = executions.find((result) => result.status === "rejected");
|
|
544
|
+
if (failed)
|
|
545
|
+
throw failed.reason;
|
|
546
|
+
const decision = await telemetry.span("client.step_complete", {}, (t) => hooks.onStepComplete ? hooks.onStepComplete({
|
|
547
|
+
...context,
|
|
548
|
+
event,
|
|
549
|
+
toolResults: results,
|
|
550
|
+
telemetry: t
|
|
551
|
+
}) : Promise.resolve(calls.length ? "continue" : "stop"));
|
|
552
|
+
if (decision === "stop")
|
|
553
|
+
return observe();
|
|
554
|
+
if (!hooks.prepareRequest)
|
|
555
|
+
throw new HookConfigurationError("prepareRequest is required to continue");
|
|
556
|
+
const input = await telemetry.span("client.prepare_request", {}, (t) => hooks.prepareRequest({
|
|
557
|
+
...context,
|
|
558
|
+
event,
|
|
559
|
+
toolResults: results,
|
|
560
|
+
telemetry: t
|
|
561
|
+
}));
|
|
462
562
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
563
|
+
...input,
|
|
564
|
+
idempotencyKey: event.id,
|
|
565
|
+
inReplyTo: { requestId: event.requestId, eventId: event.id },
|
|
566
|
+
telemetry: telemetry.snapshot()
|
|
465
567
|
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
signal?.throwIfAborted();
|
|
570
|
+
if (!hooks.onError) {
|
|
571
|
+
if (!(error instanceof HookConfigurationError))
|
|
572
|
+
throw error;
|
|
573
|
+
telemetry.log("error", error.message, { code: "invalid_hook_configuration" });
|
|
574
|
+
return { ...observe(), error: { code: error.code, message: error.message } };
|
|
575
|
+
}
|
|
576
|
+
await telemetry.span("client.error", { error: callerErrorDetails(error) }, (t) => hooks.onError(error, { ...context, telemetry: t }));
|
|
577
|
+
return observe();
|
|
466
578
|
}
|
|
467
|
-
|
|
468
|
-
|
|
579
|
+
}
|
|
580
|
+
async function prepareResponse(event, hooks, signal) {
|
|
581
|
+
const telemetry = new ClientTelemetry;
|
|
582
|
+
const response = await telemetry.span("client.processing", { eventId: event.id, eventType: event.type }, (child) => prepareResponseBody(event, hooks, child, signal));
|
|
583
|
+
if (response)
|
|
584
|
+
response.telemetry = telemetry.snapshot();
|
|
585
|
+
return response;
|
|
586
|
+
}
|
|
587
|
+
async function processEvent(client, store, event, hooks, signal) {
|
|
588
|
+
let stage = "prepare";
|
|
589
|
+
try {
|
|
590
|
+
const response = await store.prepare(event, () => prepareResponse(event, hooks, signal));
|
|
591
|
+
let receipt = null;
|
|
592
|
+
if (response) {
|
|
593
|
+
stage = "submit";
|
|
594
|
+
try {
|
|
595
|
+
receipt = await client.requests.submit(response, signal);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
if (error instanceof ConstellationError && PERMANENT_RESPONSE_CODES.has(error.code)) {
|
|
598
|
+
const rejection = new PermanentEventError(error.status, error.code, error.message);
|
|
599
|
+
await store.reject(event.id, rejection);
|
|
600
|
+
throw rejection;
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
if ("action" in response && response.action === "observe" && response.error) {
|
|
605
|
+
const failure = new HookConfigurationError(response.error.message);
|
|
606
|
+
await store.reject(event.id, failure);
|
|
607
|
+
throw failure;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
stage = "checkpoint";
|
|
611
|
+
await store.complete(event.id, receipt);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (!(error instanceof PermanentEventError) && store.retry) {
|
|
614
|
+
const detail = error instanceof ConstellationError ? { status: error.status, code: error.code, message: error.message } : { status: 0, code: error instanceof Error && error.name === "TimeoutError" ? "timeout" : stage === "submit" ? "transport_error" : `event_${stage}_failed`, message: error instanceof Error ? error.message : "Event processing failed" };
|
|
615
|
+
try {
|
|
616
|
+
await store.retry(event.id, detail);
|
|
617
|
+
} catch (monitorError) {
|
|
618
|
+
if (error && typeof error === "object")
|
|
619
|
+
Object.assign(error, { retryHookError: monitorError });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
469
623
|
}
|
|
470
624
|
}
|
|
471
625
|
|
|
@@ -491,6 +645,7 @@ class Constellation extends RequestClient {
|
|
|
491
645
|
super({
|
|
492
646
|
baseUrl: options.baseUrl,
|
|
493
647
|
timeoutMs: options.timeoutMs,
|
|
648
|
+
compressRequests: options.compressRequests,
|
|
494
649
|
fetch: authenticatedFetch
|
|
495
650
|
});
|
|
496
651
|
this.#baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
@@ -598,15 +753,14 @@ class Constellation extends RequestClient {
|
|
|
598
753
|
const event = body;
|
|
599
754
|
if (typeof event.requestId !== "string" || !event.requestId)
|
|
600
755
|
throw new Error("Invalid Constellation request ID");
|
|
601
|
-
if (typeof event.
|
|
602
|
-
if (
|
|
603
|
-
|
|
756
|
+
if (typeof event.id === "string" && event.id && Number.isSafeInteger(event.sequence) && typeof event.createdAt === "string" && Number.isFinite(Date.parse(event.createdAt)) && event.data && typeof event.data === "object" && !Array.isArray(event.data) && ["model.completed", "model.failed", "model.cancelled"].includes(String(event.type))) {
|
|
757
|
+
if (event.type === "model.completed") {
|
|
758
|
+
const data = event.data;
|
|
759
|
+
if (!data.message || typeof data.message !== "object" || data.message.role !== "assistant" || !data.result || typeof data.result !== "object")
|
|
760
|
+
throw new Error("Invalid completed model result");
|
|
761
|
+
}
|
|
604
762
|
return event;
|
|
605
763
|
}
|
|
606
|
-
if (event.kind === "request.events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
|
|
607
|
-
return event;
|
|
608
|
-
if (["request.completed", "request.failed", "request.cancelled"].includes(String(event.kind)) && Number.isSafeInteger(event.eventSeq))
|
|
609
|
-
return event;
|
|
610
764
|
throw new Error("Unknown Constellation webhook event");
|
|
611
765
|
}
|
|
612
766
|
};
|
|
@@ -614,6 +768,14 @@ class Constellation extends RequestClient {
|
|
|
614
768
|
}
|
|
615
769
|
export {
|
|
616
770
|
uuid7,
|
|
771
|
+
processEvent,
|
|
772
|
+
prepareResponse,
|
|
773
|
+
normalizeTimestamp,
|
|
774
|
+
PermanentEventError,
|
|
775
|
+
PERMANENT_RESPONSE_CODES,
|
|
776
|
+
HookConfigurationError,
|
|
777
|
+
EventReceiver,
|
|
617
778
|
ConstellationError,
|
|
618
|
-
Constellation
|
|
779
|
+
Constellation,
|
|
780
|
+
ClientTelemetry
|
|
619
781
|
};
|