@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 +25 -5
- package/dist/index.js +49 -4
- package/dist/types/client-core/src/index.d.ts +36 -0
- package/dist/types/sdk/src/index.d.ts +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -273,13 +273,13 @@ async function handle(event: WebhookEvent) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
//
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|