@lionhummer/constellation 0.9.0 → 0.9.2

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
@@ -273,13 +273,13 @@ async function handle(event: WebhookEvent) {
273
273
  }
274
274
  }
275
275
 
276
- // Resume unfinished events after a restart or a failure.
277
- setInterval(async () => {
278
- for (const event of await store.pending()) await handle(event);
279
- }, 60_000);
276
+ // Optional: resume unfinished events right after a restart.
277
+ for (const event of await store.pending()) void handle(event);
280
278
  ```
281
279
 
282
- You can also process inside the webhook and return `204` when `processEvent` finishes. That only works when your tools are fast: Constellation waits 5 seconds for the webhook response by default, then retries delivery. A retry is safe, because `prepare` serializes the work, but slow handlers turn into repeated deliveries.
280
+ Don't run tools inside the webhook call. Constellation waits 5 seconds for the acknowledgement by default, and nothing in Constellation waits for your tools.
281
+
282
+ If your process dies while handling an event, you don't need a poller to recover it. Constellation sends an unanswered `model.completed` event again after the response window (10 minutes by default), up to 3 times, and stops as soon as your response arrives. A re-sent event has the same `id`, so `store.accept` and `store.prepare` resume it from your saved checkpoint. If the event is still being processed when the re-send arrives, acknowledge it anyway. Keep your processing lease shorter than the response window and renew it while work continues, so a re-send after a crash can take the event over.
283
283
 
284
284
  ## What `processEvent` does
285
285
 
@@ -343,11 +343,30 @@ tools: {
343
343
 
344
344
  The SDK records `client.processing` and one span per tool automatically. A response carries at most 512 spans, 128 logs and 1 MiB of telemetry; the SDK trims larger batches and records what it omitted. Do not put secrets or personal data in span names, attributes or log messages.
345
345
 
346
+ ## Opening lines
347
+
348
+ Show a one-sentence opener while the real step is prepared. The operator configures its model and prompt in Constellation.
349
+
350
+ ```ts
351
+ const opener = await client.openers.create({
352
+ idempotencyKey: `${turnId}`,
353
+ workspaceId, userId, externalId: threadId,
354
+ message,
355
+ history: recentTurns, // [{ role: "user" | "assistant", text }], oldest first
356
+ })
357
+ if (opener) void client.openers.sentence(opener.id).then((sentence) => {
358
+ if (sentence && !realOutputStarted) show(sentence)
359
+ })
360
+ ```
361
+
362
+ Start the real request without waiting for the opener. `parseOpenerSentence(text)` applies the same checks to text you already have.
363
+
346
364
  ## Limits and timeouts
347
365
 
348
366
  | Setting | Default | Notes |
349
367
  | --- | --- | --- |
350
368
  | `timeoutMs` | 30 minutes | Deadline for the whole request, including your processing between steps. |
369
+ | `compressRequests` client option | `true` | Request bodies of 16 KiB or more are sent gzip-compressed. Every step resends the full conversation, so this cuts upload time several-fold. |
351
370
  | `attemptTimeoutMs`, `firstByteTimeoutMs`, `idleTimeoutMs` | not set | Optional per-provider-call limits. Operators also run stall watchdogs: by default 120 s to the first response byte and 300 s with no data. Keepalives count as data. |
352
371
  | `maxProviderAttempts` | 32 | Provider attempts per step, including fallbacks (1–256). |
353
372
  | `toolConcurrency` hook option | 8 | Parallel tool calls per event (1–128). |
@@ -366,6 +385,7 @@ A failed provider call falls back to the next provider only before any output ha
366
385
  | `requests.submit(input)` | Start a request, continue it (`inReplyTo`), or stop it (`action: "observe"`). |
367
386
  | `requests.preview(input)` | Validate a complete input and show routing, without calling a provider. |
368
387
  | `requests.cancel(id)` | Cancel a request. |
388
+ | `openers.create(input)`, `openers.sentence(id)` | Start an opening line for a just-sent message (null when openers are turned off), then wait for its sentence (null when skipped or failed). |
369
389
  | `requests.getState(id)` | Committed state: status, steps, attempt outcomes, usage and cost. |
370
390
  | `requests.getSummary(id)`, `requests.get(id)` | Request detail, without or with spans and logs. |
371
391
  | `requests.list(query)` | Page through requests, e.g. by `externalId` or `status`. |
package/dist/index.js CHANGED
@@ -1,4 +1,15 @@
1
1
  // ../client-core/src/index.ts
2
+ function parseOpenerSentence(raw, maxWords = 25) {
3
+ const text = raw.trim();
4
+ if (!text || /^SKIP\b/i.test(text))
5
+ return null;
6
+ const sentence = (text.match(/^.+?[.!](?=\s|[A-Z]|$)/)?.[0] ?? text).trim();
7
+ if (sentence.includes("`") || sentence.includes(`
8
+ `) || sentence.split(/\s+/).length > maxWords)
9
+ return null;
10
+ return sentence;
11
+ }
12
+
2
13
  class ConstellationError extends Error {
3
14
  status;
4
15
  code;
@@ -102,6 +113,15 @@ async function pause(ms, signal) {
102
113
  signal?.addEventListener("abort", abort, { once: true });
103
114
  });
104
115
  }
116
+ var COMPRESS_FROM_BYTES = 16 * 1024;
117
+ async function encodeBody(body, compress) {
118
+ const json = JSON.stringify(body);
119
+ if (!compress || json.length < COMPRESS_FROM_BYTES || typeof CompressionStream === "undefined") {
120
+ return { body: json, headers: { "Content-Type": "application/json" } };
121
+ }
122
+ const gzipped = await new Response(new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();
123
+ return { body: gzipped, headers: { "Content-Type": "application/json", "Content-Encoding": "gzip" } };
124
+ }
105
125
 
106
126
  class RequestClient {
107
127
  #fetcher;
@@ -115,12 +135,11 @@ class RequestClient {
115
135
  return `${this.#options.baseUrl.replace(/\/$/, "")}${route}`;
116
136
  }
117
137
  async#call(path, method = "GET", body, signal) {
138
+ const encoded = body === undefined ? undefined : await encodeBody(body, this.#options.compressRequests ?? true);
118
139
  const response = await this.#fetcher(this.#url(path), {
119
140
  method,
120
- headers: {
121
- ...body !== undefined ? { "Content-Type": "application/json" } : {}
122
- },
123
- body: body === undefined ? undefined : JSON.stringify(body),
141
+ headers: encoded?.headers ?? {},
142
+ body: encoded?.body,
124
143
  signal: signal ? AbortSignal.any([
125
144
  signal,
126
145
  AbortSignal.timeout(this.#options.timeoutMs ?? 30000)
@@ -292,6 +311,30 @@ class RequestClient {
292
311
  resolve: (input, signal) => this.#call("/v1/registry/resolve", "POST", input, signal)
293
312
  };
294
313
  }
314
+ get openers() {
315
+ return {
316
+ create: async (input, signal) => await this.#call("/v1/openers", "POST", input, signal) ?? null,
317
+ sentence: async (id, options = {}) => {
318
+ let text = "";
319
+ for await (const event of this.#events(id, { signal: options.signal })) {
320
+ if (event.type === "model.completed") {
321
+ const message = event.data.message;
322
+ text = message?.text ?? text;
323
+ break;
324
+ }
325
+ if (event.type === "model.failed" || event.type === "model.cancelled")
326
+ return null;
327
+ if (event.type === "model.delta") {
328
+ for (const entry of event.data.events ?? []) {
329
+ if (entry.event?.type === "text_delta")
330
+ text += entry.event.delta ?? "";
331
+ }
332
+ }
333
+ }
334
+ return parseOpenerSentence(text, options.maxWords);
335
+ }
336
+ };
337
+ }
295
338
  get requests() {
296
339
  return {
297
340
  submit: (input, signal) => this.#submit(input, signal),
@@ -637,6 +680,7 @@ class Constellation extends RequestClient {
637
680
  super({
638
681
  baseUrl: options.baseUrl,
639
682
  timeoutMs: options.timeoutMs,
683
+ compressRequests: options.compressRequests,
640
684
  fetch: authenticatedFetch
641
685
  });
642
686
  this.#baseUrl = options.baseUrl.replace(/\/$/, "");
@@ -761,6 +805,7 @@ export {
761
805
  uuid7,
762
806
  processEvent,
763
807
  prepareResponse,
808
+ parseOpenerSentence,
764
809
  normalizeTimestamp,
765
810
  PermanentEventError,
766
811
  PERMANENT_RESPONSE_CODES,
@@ -304,6 +304,24 @@ export type ModelDeltaEvent = {
304
304
  };
305
305
  };
306
306
  export type RequestEvent = WebhookEvent | ModelDeltaEvent;
307
+ export type OpenerTurn = {
308
+ role: "user" | "assistant";
309
+ text: string;
310
+ };
311
+ export type OpenerInput = {
312
+ /** Stable per turn, so a retry never admits a second opener. */
313
+ idempotencyKey: string;
314
+ workspaceId: string;
315
+ userId?: string;
316
+ externalId?: string;
317
+ planId?: string;
318
+ /** The message the user just sent. */
319
+ message: string;
320
+ /** Earlier turns, oldest first; operator settings decide how many are used. */
321
+ history?: OpenerTurn[];
322
+ };
323
+ /** The opener line to show: one sentence, no code, within `maxWords`; null for SKIP or anything else. */
324
+ export declare function parseOpenerSentence(raw: string, maxWords?: number): string | null;
307
325
  export type Page<T> = {
308
326
  items: T[];
309
327
  total: number;
@@ -492,6 +510,8 @@ type ClientOptions = {
492
510
  apiPrefix?: "/v1" | "/internal";
493
511
  fetch?: typeof fetch;
494
512
  timeoutMs?: number;
513
+ /** gzip JSON bodies of at least 16 KiB (default true). */
514
+ compressRequests?: boolean;
495
515
  };
496
516
  export declare class RequestClient {
497
517
  #private;
@@ -501,6 +521,22 @@ export declare class RequestClient {
501
521
  list: (scope: ModelScope, signal?: AbortSignal) => Promise<Json>;
502
522
  resolve: (input: ModelResolution, signal?: AbortSignal) => Promise<Json>;
503
523
  };
524
+ /**
525
+ * Opening lines: an operator-configured short model call, made when the user
526
+ * sends, naming what the assistant does first. create admits it as an
527
+ * ordinary request; sentence waits for it to end and returns the line to
528
+ * show, or null for SKIP, a failure, or an answer that breaks the format.
529
+ */
530
+ get openers(): {
531
+ /** The opener request, or null when the operator has turned openers off. */
532
+ create: (input: OpenerInput, signal?: AbortSignal) => Promise<{
533
+ id: string;
534
+ } | null>;
535
+ sentence: (id: string, options?: {
536
+ signal?: AbortSignal;
537
+ maxWords?: number;
538
+ }) => Promise<string | null>;
539
+ };
504
540
  /** Durable requests. create returns an ID; execution continues independently. */
505
541
  get requests(): {
506
542
  submit: (input: Admission, signal?: AbortSignal) => Promise<AdmissionReceipt>;
@@ -1,7 +1,7 @@
1
1
  /** External server SDK. Operator APIs and cookie sessions are internal-only clients. */
2
2
  import { RequestClient, type WebhookEvent, type Json, type Page, type PageQuery, type ModelResolution, type RequestRecord, type RequestEvent, type RecordTypes, type Timeline, type TimelineRow, type RequestListQuery, type RequestRecordsQuery, type RequestTimings, type TokenUsage } from "../../client-core/src/index";
3
- export { ConstellationError, uuid7, normalizeTimestamp } from "../../client-core/src/index";
4
- export type { InferenceParameters, Json, Tool, ToolCall, Message, RequestEventModes, RequestInput, Admission, AdmissionReceipt, ResponseOutcome, ObservationResponse, Observations, EventReference, ModelCompletedEvent, ModelDeltaEvent, ModelStreamFrame, RequestTimings, TimingInterval, TokenUsage, TokenPrice, CustomerPricing, BillingFacts, Status, RequestRecord, Step, Attempt, Span, TimelineRow, Timeline, Log, Inspection, RecordTypes, RequestEvent, Page, PageQuery, ModelScope, ModelResolution, WebhookEvent, RequestRecordsQuery, RequestListQuery } from "../../client-core/src/index";
3
+ export { ConstellationError, uuid7, normalizeTimestamp, parseOpenerSentence } from "../../client-core/src/index";
4
+ export type { InferenceParameters, Json, Tool, ToolCall, Message, RequestEventModes, RequestInput, Admission, AdmissionReceipt, ResponseOutcome, ObservationResponse, Observations, EventReference, ModelCompletedEvent, ModelDeltaEvent, ModelStreamFrame, RequestTimings, TimingInterval, TokenUsage, TokenPrice, CustomerPricing, BillingFacts, Status, RequestRecord, Step, Attempt, Span, TimelineRow, Timeline, Log, Inspection, RecordTypes, RequestEvent, Page, PageQuery, ModelScope, ModelResolution, WebhookEvent, RequestRecordsQuery, RequestListQuery, OpenerInput, OpenerTurn } from "../../client-core/src/index";
5
5
  /** OpenAPI contract filtered to this key's permissions and scope. */
6
6
  export type DiscoveryDocument = {
7
7
  openapi: string;
@@ -94,6 +94,7 @@ export declare class Constellation extends RequestClient {
94
94
  apiKey: string;
95
95
  fetch?: typeof fetch;
96
96
  timeoutMs?: number;
97
+ compressRequests?: boolean;
97
98
  });
98
99
  /** Discover HTTP operations available to this bearer key. */
99
100
  discover(signal?: AbortSignal): Promise<DiscoveryDocument>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "TypeScript client for durable model requests and model events",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",