@lionhummer/constellation 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -217,3 +217,15 @@ The standard Reasoning Effort vocabulary is `none`, `minimal`, `low`, `medium`,
217
217
  For cache diagnostics, send `cache: { mode: "automatic", diagnostics: true }` on the first step. `providerResponseId` appears on attempts and in the `afterModel` checkpoint payload. Use that upstream ID as a later step’s `cache.comparisonId`, which also enables diagnostics. A successful cache hit may return no diagnostic payload. Telemetry exposes the response ID by default; raw `cacheDiagnostics` requires `include: "content"`. Diagnostics and prewarm are offering capabilities: discover support before requesting them. Invalid TTLs or unsupported required cache settings fail rather than being silently dropped.
218
218
 
219
219
  Send beta tokens as `headers: { "anthropic-beta": "your-beta-token" }`, not inside `parameters`. The provider connection must allow the header name. Unapproved, protected or malformed headers are dropped with warnings. A permitted header can still contain an upstream-unsupported beta token; the provider's own error remains visible. Cache diagnostics can add their required beta separately. Obvious may choose provider-specific prompt placement and cache boundaries while using Constellation's standard cache request.
220
+
221
+ ## Reasoning history and model handoffs
222
+
223
+ Retain the complete `nativeContent` artifact from assistant output alongside canonical text, tool calls and results. Reasoning summaries alone cannot reconstruct signed/encrypted state. Use `nativeContentPolicy: "omitIncompatible"` on submitted or prepared steps to permit cross-family handoffs, including active tool continuations. Constellation omits only incompatible native state in the outbound attempt and records a warning; the original history remains intact. `require` excludes incompatible routes; `omitCompleted` permits omission only from completed turns.
224
+
225
+ Same-protocol Claude/Responses model switches let providers verify native readability. Responses account scopes must be verified; Gemini also requires the model replay group. Preserve the exact prompt prefix for signed thinking. Provider-reported thinking drops and allowed prefix mismatches appear in provider span `attributes.reasoningTransformations`; a successful response does not prove retention. See [the HTTP contract](../../docs/api.md#provider-protocols).
226
+
227
+ ### Tool timing
228
+
229
+ `inputs.handle` and `inputs.poll` record tool-input inspection, claim and response POST phases. A tool handler receives `context.parentSpanId` and `context.recordSpan(span)`; use the latter to attach closed preparation/execution spans without an HTTP call per phase. Child spans are batched with the tool-span close. Keep spans linked to the current input and omit arguments, results and secrets from attributes. The result POST observation is best effort: a request that has already ended rejects late telemetry, without replaying the committed response.
230
+
231
+ Callback round trips include your handler's processing time. Compare the input wait, tool span and child execution phases rather than adding their overlapping durations. Monotonic `elapsedMs` values are retained in metadata-only telemetry; timestamps across hosts assume aligned clocks.
package/dist/index.js CHANGED
@@ -202,7 +202,7 @@ class RequestClient {
202
202
  status: options2.status ?? closed?.status ?? "ok",
203
203
  attributes: options2.attributes ?? closed?.attributes ?? span.attributes
204
204
  };
205
- return this.#addSpans(id, [closed], signal2);
205
+ return this.#addSpans(id, [closed, ...options2.spans ?? []], signal2);
206
206
  }
207
207
  };
208
208
  }
@@ -285,7 +285,11 @@ class RequestClient {
285
285
  async#processInputs(id, options = {}) {
286
286
  const consumerId = options.consumerId ?? uuid7();
287
287
  while (true) {
288
+ const inspectStarted = new Date().toISOString();
289
+ const inspectClock = performance.now();
288
290
  const state = await this.#inspect(id, options.signal);
291
+ const inspectEnded = new Date().toISOString();
292
+ const inspectMs = performance.now() - inspectClock;
289
293
  options.onUpdate?.(state);
290
294
  if (terminal(state.request.status))
291
295
  return state;
@@ -351,6 +355,8 @@ class RequestClient {
351
355
  }
352
356
  if (pending.kind !== "tool")
353
357
  continue;
358
+ const claimStarted = new Date().toISOString();
359
+ const claimClock = performance.now();
354
360
  const tool = options.tools?.[pending.payload.name ?? ""];
355
361
  if (!tool && !pending.payload.validationError)
356
362
  continue;
@@ -361,8 +367,14 @@ class RequestClient {
361
367
  continue;
362
368
  throw error;
363
369
  }
370
+ const claimEnded = new Date().toISOString();
371
+ const claimMs = performance.now() - claimClock;
364
372
  await this.#whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
365
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
+ ];
366
378
  let result;
367
379
  let isError = false;
368
380
  let errorDetails;
@@ -375,6 +387,8 @@ class RequestClient {
375
387
  result = await bounded(tool(pending.payload.arguments ?? {}, {
376
388
  inputId: pending.id,
377
389
  requestId: id,
390
+ parentSpanId: span.id,
391
+ recordSpan: (phase) => phases.push({ ...phase, inputId: pending.id, parentSpanId: span.id }),
378
392
  signal: toolSignal
379
393
  }), toolSignal);
380
394
  } catch (error) {
@@ -383,9 +397,23 @@ class RequestClient {
383
397
  errorDetails = callerErrorDetails(error);
384
398
  result = { error: error instanceof Error ? error.message : "Tool failed" };
385
399
  }
386
- await span.end({ status: isError ? "error" : "ok", ...errorDetails ? { attributes: { error: errorDetails } } : {} }, activeSignal);
400
+ await span.end({ status: isError ? "error" : "ok", spans: phases, ...errorDetails ? { attributes: { error: errorDetails } } : {} }, activeSignal);
387
401
  await this.#claim(id, pending.id, consumerId, activeSignal);
388
- await this.#respond(id, pending.id, { result, isError, claimId: 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
+ }
389
417
  }, options.signal);
390
418
  handled = true;
391
419
  }
@@ -57,7 +57,7 @@ type RequestFields = {
57
57
  optionalParameters?: string[];
58
58
  /** Only headers approved by the selected provider are forwarded; others produce warnings. */
59
59
  headers?: Record<string, string>;
60
- nativeContentPolicy?: "require" | "omitCompleted";
60
+ nativeContentPolicy?: "require" | "omitCompleted" | "omitIncompatible";
61
61
  maxSteps?: number;
62
62
  timeoutMs?: number;
63
63
  maxOutputTokens?: number;
@@ -183,7 +183,7 @@ export type PreparedStep = {
183
183
  optionalParameters?: string[];
184
184
  /** Only headers approved by the selected provider are forwarded; others produce warnings. */
185
185
  headers?: Record<string, string>;
186
- nativeContentPolicy?: "require" | "omitCompleted";
186
+ nativeContentPolicy?: "require" | "omitCompleted" | "omitIncompatible";
187
187
  maxOutputTokens: number;
188
188
  cache: RequestInput["cache"] | null;
189
189
  };
@@ -300,6 +300,8 @@ export type InputHandlers = {
300
300
  tools?: Record<string, (args: Json, context: {
301
301
  inputId: string;
302
302
  requestId: string;
303
+ parentSpanId: string;
304
+ recordSpan: (span: Span) => void;
303
305
  signal?: AbortSignal;
304
306
  }) => Promise<Json>>;
305
307
  userInput?: (state: Inspection, context: {
@@ -342,7 +344,7 @@ export type ModelResolution = ModelScope & {
342
344
  optionalParameters?: string[];
343
345
  /** Only headers approved by the selected provider are forwarded; others produce warnings. */
344
346
  headers?: Record<string, string>;
345
- nativeContentPolicy?: "require" | "omitCompleted";
347
+ nativeContentPolicy?: "require" | "omitCompleted" | "omitIncompatible";
346
348
  };
347
349
  export type WebhookEvent = {
348
350
  requestId: string;
@@ -571,6 +573,7 @@ export declare class RequestClient {
571
573
  endedAt?: string;
572
574
  status?: "ok" | "error" | "cancelled";
573
575
  attributes?: Record<string, Json>;
576
+ spans?: Span[];
574
577
  }, signal?: AbortSignal) => Promise<void>;
575
578
  }>;
576
579
  add: (requestId: string, spans: Span[], signal?: AbortSignal) => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript client for durable model requests and caller events",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",