@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/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
- async function bounded(promise, signal) {
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
- });
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
- ...body !== undefined ? { "Content-Type": "application/json" } : {}
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
- return this.#call("/v1/requests", "POST", input, signal);
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
- 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) {
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.seq <= after)
251
+ if (event.sequence <= after)
254
252
  continue;
255
- after = event.seq;
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.#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;
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#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 inspectStarted = new Date().toISOString();
289
- const inspectClock = performance.now();
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
- const claimEnded = new Date().toISOString();
371
- const claimMs = performance.now() - claimClock;
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
- create: (input, signal) => this.#submit(input, signal),
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
- get inputs() {
454
- return {
455
- handle: (requestId, inputId, handlers) => this.#handleInput(requestId, inputId, handlers),
456
- poll: (requestId, handlers) => this.#processInputs(requestId, handlers),
457
- claim: (requestId, inputId, claimId, signal) => this.#claim(requestId, inputId, claimId, signal),
458
- respond: (requestId, inputId, response, signal) => this.#respond(requestId, inputId, response, signal)
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
- get spans() {
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
- start: (requestId, name, options = {}, signal) => this.#startSpan(requestId, name, options, signal),
464
- add: (requestId, spans, signal) => this.#addSpans(requestId, spans, signal)
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
- get logs() {
468
- return { add: (requestId, logs, signal) => this.#addLogs(requestId, logs, signal) };
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.inputId === "string" && ["tool", "before_model", "after_model", "after_step", "user_input"].includes(String(event.kind))) {
602
- if (request.headers.get("x-constellation-input-id") !== event.inputId || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload))
603
- throw new Error("Invalid Constellation input notification");
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
  };