@lionhummer/constellation 0.6.0 → 0.9.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/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,14 +102,6 @@ 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
- });
71
- }
72
105
 
73
106
  class RequestClient {
74
107
  #fetcher;
@@ -99,8 +132,22 @@ class RequestClient {
99
132
  }
100
133
  return response.status === 204 ? undefined : response.json();
101
134
  }
102
- #submit(input, signal) {
103
- return this.#call("/v1/requests", "POST", input, signal);
135
+ async#submit(input, signal) {
136
+ const prepared = JSON.parse(JSON.stringify(input));
137
+ if (prepared.telemetry) {
138
+ prepared.telemetry = normalizeObservations(prepared.telemetry);
139
+ validateObservations(prepared.telemetry);
140
+ }
141
+ for (let attempt = 0;; attempt++) {
142
+ try {
143
+ return await this.#call("/v1/requests", "POST", prepared, signal);
144
+ } catch (error) {
145
+ signal?.throwIfAborted();
146
+ if (attempt >= 2 || error instanceof ConstellationError && error.status !== 429 && error.status < 500)
147
+ throw error;
148
+ await pause(100 * 2 ** attempt, signal);
149
+ }
150
+ }
104
151
  }
105
152
  #inspect(id, signal) {
106
153
  return this.#call(`/v1/requests/${encodeURIComponent(id)}`, "GET", undefined, signal);
@@ -132,80 +179,18 @@ class RequestClient {
132
179
  params.set(k, String(v));
133
180
  return this.#call(`/v1/requests?${params}`, "GET", undefined, signal);
134
181
  }
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;
182
+ async#cancel(id, signal) {
183
+ for (let attempt = 0;; attempt++) {
184
+ try {
185
+ return await this.#call(`/v1/requests/${encodeURIComponent(id)}/cancel`, "POST", {}, signal);
186
+ } catch (error) {
187
+ signal?.throwIfAborted();
188
+ if (attempt >= 7 || !(error instanceof ConstellationError) || error.status !== 409 || error.code !== "conflict")
189
+ throw error;
190
+ await pause(Math.min(25 * 2 ** attempt, 200), signal);
166
191
  }
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
192
  }
176
193
  }
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
194
  async* #events(id, options = {}) {
210
195
  let after = options.after ?? 0;
211
196
  while (true) {
@@ -227,6 +212,11 @@ class RequestClient {
227
212
  if (!response)
228
213
  continue;
229
214
  if (!response.ok || !response.body) {
215
+ if (response.status === 429 || response.status >= 500) {
216
+ await response.body?.cancel();
217
+ await pause(500, options.signal);
218
+ continue;
219
+ }
230
220
  throw new ConstellationError(response.status, "stream_error", `Event stream returned ${response.status}`);
231
221
  }
232
222
  const reader = response.body.pipeThrough(new TextDecoderStream).getReader();
@@ -250,12 +240,10 @@ class RequestClient {
250
240
  if (!data)
251
241
  continue;
252
242
  const event = JSON.parse(data);
253
- if (event.seq <= after)
243
+ if (event.sequence <= after)
254
244
  continue;
255
- after = event.seq;
245
+ after = event.sequence;
256
246
  yield event;
257
- if (["request.completed", "request.failed", "request.cancelled"].includes(event.type))
258
- return;
259
247
  }
260
248
  }
261
249
  } catch {
@@ -263,166 +251,39 @@ class RequestClient {
263
251
  } finally {
264
252
  await reader.cancel().catch(() => {});
265
253
  }
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;
254
+ const inspection = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/state`, options.signal);
255
+ if (["completed", "failed", "cancelled"].includes(inspection.request.status)) {
256
+ while (true) {
257
+ const before = after;
258
+ const tail = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/events?after=${after}`, options.signal);
259
+ for (const event of tail.items) {
260
+ if (event.sequence <= after)
261
+ continue;
262
+ after = event.sequence;
263
+ yield event;
264
+ }
265
+ if (tail.items.length === 0)
266
+ return;
267
+ if (after === before)
268
+ throw new Error("Replay page did not advance the event cursor");
274
269
  }
275
- if (tail.items.length < 500)
276
- return;
277
- continue;
278
270
  }
279
271
  await pause(500, options.signal);
280
272
  }
281
273
  }
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;
274
+ async#replayRead(path, signal) {
275
+ for (;; ) {
276
+ signal?.throwIfAborted();
277
+ try {
278
+ return await this.#call(path, "GET", undefined, signal);
279
+ } catch (error) {
280
+ signal?.throwIfAborted();
281
+ if (error instanceof ConstellationError && error.status !== 429 && error.status < 500)
368
282
  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);
283
+ if (error instanceof SyntaxError)
284
+ throw error;
285
+ await pause(500, signal);
424
286
  }
425
- await pause(500, options.signal);
426
287
  }
427
288
  }
428
289
  get models() {
@@ -433,9 +294,10 @@ class RequestClient {
433
294
  }
434
295
  get requests() {
435
296
  return {
436
- create: (input, signal) => this.#submit(input, signal),
297
+ submit: (input, signal) => this.#submit(input, signal),
437
298
  preview: (input, signal) => this.#resolve(input, signal),
438
299
  get: (id, signal) => this.#inspect(id, signal),
300
+ getState: (id, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/state`, "GET", undefined, signal),
439
301
  getSummary: (id, signal) => this.#inspectSummary(id, signal),
440
302
  getTimings: (id, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/timings`, "GET", undefined, signal),
441
303
  list: (query = {}, signal) => this.#list(query, signal),
@@ -450,22 +312,306 @@ class RequestClient {
450
312
  getHttpBody: (id, attemptId, query, signal) => this.#call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal)
451
313
  };
452
314
  }
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
- };
315
+ }
316
+ // ../client-core/src/error-details.ts
317
+ function callerErrorDetails(error) {
318
+ let cause = error;
319
+ const seen = new Set;
320
+ for (let depth = 0;depth < 8 && cause instanceof Error && !seen.has(cause); depth++) {
321
+ seen.add(cause);
322
+ if (!(cause.cause instanceof Error))
323
+ break;
324
+ cause = cause.cause;
460
325
  }
461
- get spans() {
326
+ const record = cause && typeof cause === "object" ? cause : {};
327
+ const name = cause instanceof Error ? cause.name.slice(0, 80) : "CallerError";
328
+ const code = typeof record.code === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(record.code) ? record.code : undefined;
329
+ const raw = cause instanceof Error ? cause.message : "Caller handler threw a non-Error value";
330
+ 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);
331
+ return { name, message, ...code ? { code } : {} };
332
+ }
333
+
334
+ // ../client-core/src/permanent-codes.json
335
+ var permanent_codes_default = [
336
+ "bad_request",
337
+ "observations_expired",
338
+ "response_conflict"
339
+ ];
340
+
341
+ // ../client-core/src/agent.ts
342
+ var PERMANENT_RESPONSE_CODES = new Set(permanent_codes_default);
343
+ function truncateUtf8(value, limit) {
344
+ let result = "", bytes = 0;
345
+ for (const raw of value) {
346
+ const point = raw.codePointAt(0);
347
+ const char = point >= 55296 && point <= 57343 ? "�" : raw;
348
+ const width = new TextEncoder().encode(char).byteLength;
349
+ if (bytes + width > limit)
350
+ break;
351
+ result += char;
352
+ bytes += width;
353
+ }
354
+ return result;
355
+ }
356
+ function cleanJson(value) {
357
+ if (typeof value === "string")
358
+ return truncateUtf8(value, Infinity);
359
+ if (Array.isArray(value))
360
+ return value.map(cleanJson);
361
+ if (value && typeof value === "object")
362
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [truncateUtf8(key, Infinity), cleanJson(item)]));
363
+ return value;
364
+ }
365
+
366
+ class PermanentEventError extends Error {
367
+ status;
368
+ code;
369
+ constructor(status, code, message) {
370
+ super(message);
371
+ this.status = status;
372
+ this.code = code;
373
+ this.name = "PermanentEventError";
374
+ }
375
+ }
376
+
377
+ class HookConfigurationError extends PermanentEventError {
378
+ constructor(message) {
379
+ super(400, "invalid_hook_configuration", message);
380
+ this.name = "HookConfigurationError";
381
+ }
382
+ }
383
+
384
+ class EventReceiver {
385
+ store;
386
+ constructor(store) {
387
+ this.store = store;
388
+ }
389
+ async receive(event) {
390
+ await this.store.accept(event);
391
+ }
392
+ }
393
+
394
+ class ClientTelemetry {
395
+ parentSpanId;
396
+ spans = [];
397
+ logs = [];
398
+ constructor(parentSpanId) {
399
+ this.parentSpanId = parentSpanId;
400
+ }
401
+ log(level, message, attributes) {
402
+ this.logs.push({
403
+ id: uuid7(),
404
+ level,
405
+ message,
406
+ timestamp: new Date().toISOString(),
407
+ spanId: this.parentSpanId,
408
+ attributes
409
+ });
410
+ }
411
+ async span(name, attributes, work) {
412
+ const id = uuid7();
413
+ const startedAt = new Date().toISOString();
414
+ const started = performance.now();
415
+ const child = new ClientTelemetry(id);
416
+ let status = "ok";
417
+ let failure;
418
+ try {
419
+ return await work(child);
420
+ } catch (error) {
421
+ status = "error";
422
+ failure = callerErrorDetails(error);
423
+ throw error;
424
+ } finally {
425
+ this.spans.push({
426
+ id,
427
+ name,
428
+ parentSpanId: this.parentSpanId,
429
+ startedAt,
430
+ endedAt: new Date().toISOString(),
431
+ status,
432
+ attributes: {
433
+ ...attributes,
434
+ elapsedMs: performance.now() - started,
435
+ ...failure ? { error: failure } : {}
436
+ }
437
+ }, ...child.spans);
438
+ this.logs.push(...child.logs);
439
+ }
440
+ }
441
+ snapshot() {
442
+ const spans = structuredClone(this.spans.slice(0, 512));
443
+ const logs = structuredClone(this.logs.slice(0, 128));
444
+ for (const span of spans) {
445
+ span.name = truncateUtf8(span.name, 256).trim() || "(unnamed span)";
446
+ if (span.attributes)
447
+ span.attributes = cleanJson(span.attributes);
448
+ if (span.endedAt && span.endedAt < span.startedAt)
449
+ span.endedAt = span.startedAt;
450
+ }
451
+ for (const log of logs) {
452
+ log.message = truncateUtf8(log.message, 8192);
453
+ if (log.attributes)
454
+ log.attributes = cleanJson(log.attributes);
455
+ if (!log.message)
456
+ log.message = "(empty log)";
457
+ }
458
+ const bytes = () => observationBytes({ spans, logs });
459
+ if (bytes() > 1024 * 1024) {
460
+ for (const item of [...spans, ...logs]) {
461
+ if (new TextEncoder().encode(JSON.stringify(item.attributes ?? {})).byteLength > 4096)
462
+ item.attributes = { omitted: true, reason: "telemetry_byte_limit" };
463
+ }
464
+ }
465
+ while (bytes() > 1024 * 1024 && (logs.length || spans.length > 1)) {
466
+ if (logs.length)
467
+ logs.pop();
468
+ else
469
+ spans.pop();
470
+ }
471
+ const ids = new Set(spans.map((span) => span.id));
472
+ for (const log of logs)
473
+ if (log.spanId && !ids.has(log.spanId))
474
+ delete log.spanId;
475
+ if (spans.length && (spans.length < this.spans.length || logs.length < this.logs.length)) {
476
+ spans[0].attributes = { ...spans[0].attributes, omittedSpans: this.spans.length - spans.length, omittedLogs: this.logs.length - logs.length };
477
+ }
478
+ while (bytes() > 1024 * 1024) {
479
+ if (logs.length)
480
+ logs.pop();
481
+ else if (spans.length > 1)
482
+ spans.pop();
483
+ else if (spans.length)
484
+ spans[0].attributes = { omitted: true, reason: "telemetry_byte_limit" };
485
+ else
486
+ break;
487
+ }
488
+ return { spans, logs };
489
+ }
490
+ }
491
+ async function prepareResponseBody(event, hooks, telemetry, signal) {
492
+ const context = { event, signal, telemetry };
493
+ const observe = () => ({
494
+ action: "observe",
495
+ idempotencyKey: event.id,
496
+ inReplyTo: { requestId: event.requestId, eventId: event.id },
497
+ telemetry: telemetry.snapshot()
498
+ });
499
+ signal?.throwIfAborted();
500
+ if (event.type !== "model.completed") {
501
+ await telemetry.span("client.model_error", { type: event.type }, async (t) => {
502
+ await hooks.onError?.(event.data, { ...context, telemetry: t });
503
+ });
504
+ return observe();
505
+ }
506
+ try {
507
+ const calls = event.data.message.toolCalls ?? [];
508
+ const ids = new Set(calls.map((c) => c.id));
509
+ if (ids.size !== calls.length)
510
+ throw new Error("Duplicate tool-call IDs in model result");
511
+ const concurrency = hooks.toolConcurrency ?? 8;
512
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 128)
513
+ throw new HookConfigurationError("toolConcurrency must be between 1 and 128");
514
+ const results = new Array(calls.length);
515
+ let next = 0;
516
+ const executions = await Promise.allSettled(Array.from({ length: Math.min(concurrency, calls.length) }, async () => {
517
+ for (;; ) {
518
+ const index = next++;
519
+ if (index >= calls.length)
520
+ return;
521
+ signal?.throwIfAborted();
522
+ const call = calls[index];
523
+ const handler = hooks.tools?.[call.name];
524
+ if (!handler)
525
+ throw new HookConfigurationError(`No handler for tool ${call.name}`);
526
+ const result = await telemetry.span(`tool.${call.name}`, { toolCallId: call.id }, (t) => handler(call.arguments, {
527
+ ...context,
528
+ telemetry: t,
529
+ toolCallId: call.id,
530
+ effectId: JSON.stringify([event.id, call.id])
531
+ }));
532
+ results[index] = { id: call.id, result };
533
+ }
534
+ }));
535
+ const failed = executions.find((result) => result.status === "rejected");
536
+ if (failed)
537
+ throw failed.reason;
538
+ const decision = await telemetry.span("client.step_complete", {}, (t) => hooks.onStepComplete ? hooks.onStepComplete({
539
+ ...context,
540
+ event,
541
+ toolResults: results,
542
+ telemetry: t
543
+ }) : Promise.resolve(calls.length ? "continue" : "stop"));
544
+ if (decision === "stop")
545
+ return observe();
546
+ if (!hooks.prepareRequest)
547
+ throw new HookConfigurationError("prepareRequest is required to continue");
548
+ const input = await telemetry.span("client.prepare_request", {}, (t) => hooks.prepareRequest({
549
+ ...context,
550
+ event,
551
+ toolResults: results,
552
+ telemetry: t
553
+ }));
462
554
  return {
463
- start: (requestId, name, options = {}, signal) => this.#startSpan(requestId, name, options, signal),
464
- add: (requestId, spans, signal) => this.#addSpans(requestId, spans, signal)
555
+ ...input,
556
+ idempotencyKey: event.id,
557
+ inReplyTo: { requestId: event.requestId, eventId: event.id },
558
+ telemetry: telemetry.snapshot()
465
559
  };
560
+ } catch (error) {
561
+ signal?.throwIfAborted();
562
+ if (!hooks.onError) {
563
+ if (!(error instanceof HookConfigurationError))
564
+ throw error;
565
+ telemetry.log("error", error.message, { code: "invalid_hook_configuration" });
566
+ return { ...observe(), error: { code: error.code, message: error.message } };
567
+ }
568
+ await telemetry.span("client.error", { error: callerErrorDetails(error) }, (t) => hooks.onError(error, { ...context, telemetry: t }));
569
+ return observe();
466
570
  }
467
- get logs() {
468
- return { add: (requestId, logs, signal) => this.#addLogs(requestId, logs, signal) };
571
+ }
572
+ async function prepareResponse(event, hooks, signal) {
573
+ const telemetry = new ClientTelemetry;
574
+ const response = await telemetry.span("client.processing", { eventId: event.id, eventType: event.type }, (child) => prepareResponseBody(event, hooks, child, signal));
575
+ if (response)
576
+ response.telemetry = telemetry.snapshot();
577
+ return response;
578
+ }
579
+ async function processEvent(client, store, event, hooks, signal) {
580
+ let stage = "prepare";
581
+ try {
582
+ const response = await store.prepare(event, () => prepareResponse(event, hooks, signal));
583
+ let receipt = null;
584
+ if (response) {
585
+ stage = "submit";
586
+ try {
587
+ receipt = await client.requests.submit(response, signal);
588
+ } catch (error) {
589
+ if (error instanceof ConstellationError && PERMANENT_RESPONSE_CODES.has(error.code)) {
590
+ const rejection = new PermanentEventError(error.status, error.code, error.message);
591
+ await store.reject(event.id, rejection);
592
+ throw rejection;
593
+ }
594
+ throw error;
595
+ }
596
+ if ("action" in response && response.action === "observe" && response.error) {
597
+ const failure = new HookConfigurationError(response.error.message);
598
+ await store.reject(event.id, failure);
599
+ throw failure;
600
+ }
601
+ }
602
+ stage = "checkpoint";
603
+ await store.complete(event.id, receipt);
604
+ } catch (error) {
605
+ if (!(error instanceof PermanentEventError) && store.retry) {
606
+ 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" };
607
+ try {
608
+ await store.retry(event.id, detail);
609
+ } catch (monitorError) {
610
+ if (error && typeof error === "object")
611
+ Object.assign(error, { retryHookError: monitorError });
612
+ }
613
+ }
614
+ throw error;
469
615
  }
470
616
  }
471
617
 
@@ -598,15 +744,14 @@ class Constellation extends RequestClient {
598
744
  const event = body;
599
745
  if (typeof event.requestId !== "string" || !event.requestId)
600
746
  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");
747
+ 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))) {
748
+ if (event.type === "model.completed") {
749
+ const data = event.data;
750
+ if (!data.message || typeof data.message !== "object" || data.message.role !== "assistant" || !data.result || typeof data.result !== "object")
751
+ throw new Error("Invalid completed model result");
752
+ }
604
753
  return event;
605
754
  }
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
755
  throw new Error("Unknown Constellation webhook event");
611
756
  }
612
757
  };
@@ -614,6 +759,14 @@ class Constellation extends RequestClient {
614
759
  }
615
760
  export {
616
761
  uuid7,
762
+ processEvent,
763
+ prepareResponse,
764
+ normalizeTimestamp,
765
+ PermanentEventError,
766
+ PERMANENT_RESPONSE_CODES,
767
+ HookConfigurationError,
768
+ EventReceiver,
617
769
  ConstellationError,
618
- Constellation
770
+ Constellation,
771
+ ClientTelemetry
619
772
  };