@lionhummer/constellation 0.1.2 → 0.2.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
@@ -1,155 +1,81 @@
1
- # Constellation TypeScript SDK
1
+ # @lionhummer/constellation
2
2
 
3
- Constellation runs durable model requests and agent steps. This SDK submits requests, streams events, handles caller waits, and records spans and logs. Your application owns its conversation, tool implementations, user interaction, and any accounting or policy decisions.
4
-
5
- ## Install and connect
3
+ TypeScript client for Constellation's durable request API. Run it on a trusted server with an execution key. Constellation routes model calls and stores the execution trace; your application supplies messages, tool implementations, optional policy decisions, and billing.
6
4
 
7
5
  ```bash
8
6
  npm install @lionhummer/constellation
9
7
  ```
10
8
 
11
- Use an execution API key from a trusted server. The SDK sends it as a bearer token to the `/v1` API.
12
-
13
9
  ```ts
14
- import { Constellation, uuid7 } from "@lionhummer/constellation";
10
+ import { Constellation, uuid7 } from "@lionhummer/constellation"
15
11
 
16
12
  const client = new Constellation({
17
- baseUrl: "https://constellation.example.com",
18
- apiKey: process.env.CONSTELLATION_API_KEY,
19
- });
20
- ```
21
-
22
- The default HTTP timeout is 30 seconds per call. Pass `timeoutMs` to the constructor or an `AbortSignal` to an individual method to change it. Keep API keys out of browser code.
13
+ baseUrl: "https://your-constellation-host.example",
14
+ apiKey: process.env.CONSTELLATION_EXECUTION_KEY!,
15
+ })
23
16
 
24
- ## Make a request
25
-
26
- `generate` makes one model call. `run` can pause for tool results or a context refresh. `agent` gives the caller control before every model step and after each step's output.
27
-
28
- ```ts
29
- const { id } = await client.submit({
17
+ const { id } = await client.requests.create({
30
18
  idempotencyKey: uuid7(),
31
19
  workspaceId: "workspace-123",
32
20
  externalId: "conversation-456",
33
21
  model: "catalog/example-model",
34
- kind: "generate",
35
- messages: [{ role: "user", text: "Explain the result." }],
36
- });
37
-
38
- for await (const event of client.events(id)) {
39
- console.log(event.seq, event.type, event.data);
40
- }
41
-
42
- const final = await client.inspect(id);
43
- console.log(final.request.status, final.request.output);
22
+ instructions: "Answer accurately.",
23
+ messages: [{ role: "user", text: "Look this up." }],
24
+ tools: [{
25
+ name: "lookup",
26
+ inputSchema: { type: "object", properties: { key: { type: "string" } } },
27
+ }],
28
+ events: { afterStep: "wait", outputDelta: "notify", terminal: "notify" },
29
+ })
44
30
  ```
45
31
 
46
- Persist `idempotencyKey` and reuse it if submission is retried. `externalId` groups and finds requests; it does not carry conversation history into the next request. To resume the event stream, persist the last event's `seq` and call `events(id, { after: lastSeq })`. The stream uses SSE and replays from a durable cursor after a disconnect. Disconnecting does not cancel the request; call `cancel(id)` to do that.
32
+ The first model step uses the submitted input. Keep the idempotency key for retries; reusing it with different input is rejected. `externalId` connects independent requests and may supply a routing affinity hint; it does not load conversation history.
47
33
 
48
- ## Run an agent
34
+ ## Events and caller work
49
35
 
50
- An agent request starts with a `prepare_step` wait. Supply the **complete** step input there, including conversation messages and any updated context. Constellation executes the model call, then sends a `caller_review` wait with usage, cost, output, and pending tool calls. Tool handlers run only after the caller returns `continue`. After tools finish, a second review lets the caller choose another step or complete the request.
36
+ All event modes default to `off`. `beforeModel`, `afterModel`, and `afterStep` accept `off`, `notify`, or `wait`. `outputDelta` and `terminal` accept `off` or `notify`. Notifications are delivered to the execution key's configured webhook. A wait creates a durable input that your application must claim and answer. Tool calls always create durable inputs, including parallel calls.
51
37
 
52
- ```ts
53
- import type { Message } from "@lionhummer/constellation";
54
-
55
- const history: Message[] = [{ role: "user", text: "Find the answer." }];
56
- const { id } = await client.submit({
57
- idempotencyKey: uuid7(),
58
- workspaceId: "workspace-123",
59
- model: "catalog/example-model",
60
- kind: "agent",
61
- maxSteps: 4,
62
- prepareStep: true,
63
- messages: history,
64
- });
65
-
66
- const final = await client.drive(id, {
67
- consumerId: uuid7(), // Persist this ID if this consumer may reconnect.
68
-
69
- prepareStep: async (state, { signal }) => {
70
- signal.throwIfAborted();
71
- return {
72
- stepInput: {
73
- model: state.request.requestedModel,
74
- instructions: "Answer accurately and cite the tool result.",
75
- messages: [...history],
76
- tools: [{
77
- name: "lookup",
78
- inputSchema: {
79
- type: "object",
80
- properties: { query: { type: "string" } },
81
- required: ["query"],
82
- additionalProperties: false,
83
- },
84
- }],
85
- parameters: {},
86
- maxOutputTokens: 1024,
87
- cache: null,
88
- },
89
- };
90
- },
91
-
92
- callerReview: async (review, _state, { inputId, signal }) => {
93
- signal.throwIfAborted();
94
- if (review.stage === "after_model") {
95
- // If you account for usage, settle this review once using inputId.
96
- // Usage, cost, output and tool calls are available before tool dispatch.
97
- const output = review.output as { message?: string } | undefined;
98
- history.push({
99
- role: "assistant", text: output?.message ?? "",
100
- toolCalls: review.toolCalls ?? [],
101
- });
102
- return { action: review.toolCalls?.length ? "continue" : "complete" };
103
- }
104
- // after_tools contains tool results, not a second model charge.
105
- history.push({ role: "tool", toolResults: review.toolResults ?? [] });
106
- return { action: "continue" };
107
- },
38
+ `afterStep` happens after the model when it has no tool calls, or after all tool results. It includes output, usage, cost, and tool results. Use it to settle billing once and decide whether to complete or continue. Continuing requires a complete next `stepInput`, including model, messages, instructions, tools, parameters, output limit, and cache policy. Constellation routes each later step separately.
108
39
 
40
+ ```ts
41
+ const handlers = {
42
+ consumerId: "stable-worker-id",
109
43
  tools: {
110
- lookup: async (args, { inputId, signal }) => {
111
- signal?.throwIfAborted();
112
- // Deduplicate external side effects by inputId.
113
- return { answer: JSON.stringify(args) };
44
+ lookup: async (args, { inputId, signal }) => lookupOnce(args, inputId, signal),
45
+ },
46
+ checkpoints: {
47
+ afterStep: async (payload, state, { inputId, parentSpanId, signal }) => {
48
+ await settleOnce(inputId, payload, { parentSpanId, signal })
49
+ return shouldContinue(state)
50
+ ? { action: "continue", stepInput: await buildCompleteNextStep(state) }
51
+ : { action: "complete" }
114
52
  },
115
53
  },
116
- });
54
+ }
117
55
 
118
- console.log(final.request.status, final.steps);
56
+ await client.inputs.handle(id, inputId, handlers)
119
57
  ```
120
58
 
121
- Build `stepInput.messages` from your current conversation each time `prepareStep` runs. Adapt the example’s text-only history handling if your provider returns richer content. Make history updates idempotent by `inputId` so a retried review does not duplicate messages. The review handler may return `continue`, `complete`, `wait_for_user`, or `fail`. A `wait_for_user` action creates a durable `user_input` wait; provide `userInput` to `drive()` or claim and respond to that input manually. Use `toolTimeoutMs` to bound callbacks if they need more than the 30-second default.
122
-
123
- `drive()` polls request inspection to find pending inputs. It claims them before invoking callbacks, renews agent claims while callbacks run, and writes the response. Agent claims have a 60-second lease. A replacement consumer can take over after expiry, so use each `inputId` as an idempotency key for accounting or external tool effects. Reuse a stable `consumerId` when reconnecting the same consumer.
59
+ `inputs.handle` processes one named input, which suits webhook delivery. `inputs.poll` continuously inspects and handles inputs until terminal and must be started explicitly. Both claim and renew inputs while handlers run. Side effects should be deduplicated by input ID because delivery and responses can be retried. Low-level `inputs.claim` and `inputs.respond` are available when you manage the lifecycle yourself.
124
60
 
125
- ## Record caller work
61
+ ## Webhook
126
62
 
127
- Spans can measure preparation, tool work, or a caller review. Set `inputId` to nest a span under that wait in the request timeline. The SDK's `drive()` helper automatically creates spans around its preparation, review, and tool callbacks; you can add your own:
63
+ Configure an HTTPS callback URL and token on the execution key. Verify `x-constellation-webhook-token`; return 2xx after your effects are durable. A non-2xx response is retried. Model text/reasoning chunks arrive as `request.events` when `outputDelta` is `notify`; terminal notification carries a request ID and event sequence.
128
64
 
129
65
  ```ts
130
- const span = await client.startSpan(id, "application.context_lookup", {
131
- inputId: "pending-input-id",
132
- startedAt: new Date().toISOString(),
133
- });
134
-
135
- // Perform the work and close the span before resolving the wait.
136
- await span.end({ status: "ok" });
66
+ const event = await client.webhooks.verify(request, webhookToken)
67
+ if ("inputId" in event) {
68
+ await client.inputs.handle(event.requestId, event.inputId, handlers)
69
+ } else if (event.kind === "request.events") {
70
+ await storeEventBatchOnce(event.requestId, event.afterSeq, event.events)
71
+ } else {
72
+ await finalizeOnce(event.requestId, event.eventSeq,
73
+ await client.requests.getSummary(event.requestId))
74
+ }
137
75
  ```
138
76
 
139
- Use `addSpans()` and `addLogs()` for batches. Caller timestamps are accepted, including work that started before submission. Submit and close spans before the request becomes terminal; later writes return HTTP 409.
140
-
141
- ## Inspect and recover
77
+ ## Inspection and other APIs
142
78
 
143
- | Method | Purpose |
144
- | --- | --- |
145
- | `submit(input)` / `resolve(input)` | Submit a durable request, or preview routing and rules without dispatching. |
146
- | `events(id, { after, signal })` | Stream live events with durable cursor replay. |
147
- | `inspect(id)` / `inspectSummary(id)` | Read the full request or a lightweight summary. |
148
- | `records(id, kind, query)` | Page `steps`, `attempts`, `inputs`, `spans`, or `logs`. |
149
- | `list(query)` | Page and filter requests. |
150
- | `drive(id, options)` | Handle pending preparation, review, tool, and user input waits until terminal. |
151
- | `claim(id, inputId, consumerId)` / `respond(id, inputId, response)` | Handle a wait manually. |
152
- | `startSpan()` / `addSpans()` / `addLogs()` | Attach caller telemetry. |
153
- | `cancel(id)` | Cancel a request explicitly. |
79
+ `requests.get` returns the request and its records. For long executions, use `requests.getSummary`, `requests.listRecords`, `requests.getTimeline`, and the paginated request list. `requests.listEvents` reads the durable event journal; `requests.streamEvents` reconnects SSE from a cursor. Stopping an observer does not cancel execution; use `requests.cancel` explicitly.
154
80
 
155
- For large histories, use `inspectSummary(id)` and page the needed records, for example `records(id, "logs", { level: "error", limit: 25, offset: 0 })`. `ConstellationError` exposes the HTTP `status`, server `code`, and message. See the [execution contract](https://github.com/FlatFilers/constellation/blob/main/docs/execution.md) for request states, caching, limits, and recovery semantics.
81
+ Execution methods are top-level (`models`, `requests`, `inputs`, `spans`, `logs`). Configuration lives under `management`; read-only operational data lives under `telemetry`. The same API timeline is shown in the console. Caller spans can attach to a wait using `inputId` and nest beneath a span using `parentSpanId`.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Durable request client. No provider credentials or agent definitions live here. */
1
+ /** Durable request client. No provider credentials or application policy live here. */
2
2
  export type Json = null | boolean | number | string | Json[] | {
3
3
  [key: string]: Json;
4
4
  };
@@ -15,8 +15,12 @@ export type ToolCall = {
15
15
  export type Message = {
16
16
  role: "system" | "user" | "assistant" | "tool";
17
17
  text?: string;
18
- /** Ordered text parts, used when a cache point must end within a message. */
19
- contentParts?: string[];
18
+ /** Ordered text and HTTPS media references. Media bytes are never submitted. */
19
+ contentParts?: (string | {
20
+ type: "image" | "file";
21
+ url: string;
22
+ mediaType: string;
23
+ })[];
20
24
  nativeContent?: Json;
21
25
  toolCalls?: ToolCall[];
22
26
  toolResults?: {
@@ -25,14 +29,13 @@ export type Message = {
25
29
  isError?: boolean;
26
30
  }[];
27
31
  };
28
- export type RequestInput = {
32
+ type RequestFields = {
29
33
  idempotencyKey: string;
30
34
  externalId?: string;
31
35
  workspaceId: string;
32
36
  userId?: string;
33
37
  planId?: string;
34
38
  model: string;
35
- kind: "generate" | "run" | "agent";
36
39
  instructions?: string;
37
40
  messages: Message[];
38
41
  tools?: Tool[];
@@ -40,7 +43,6 @@ export type RequestInput = {
40
43
  maxSteps?: number;
41
44
  timeoutMs?: number;
42
45
  maxOutputTokens?: number;
43
- prepareStep?: boolean;
44
46
  cache?: {
45
47
  mode: "automatic" | "explicit";
46
48
  points?: ({
@@ -54,13 +56,23 @@ export type RequestInput = {
54
56
  isolationKey?: string;
55
57
  };
56
58
  };
59
+ export type RequestEventModes = {
60
+ beforeModel?: "off" | "notify" | "wait";
61
+ afterModel?: "off" | "notify" | "wait";
62
+ afterStep?: "off" | "notify" | "wait";
63
+ outputDelta?: "off" | "notify";
64
+ terminal?: "off" | "notify";
65
+ };
66
+ /** One execution protocol; events determine where the caller participates. */
67
+ export type RequestInput = RequestFields & {
68
+ events?: RequestEventModes;
69
+ };
57
70
  export type Status = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled";
58
71
  export type RequestRecord = {
59
72
  id: string;
60
73
  externalId: string | null;
61
74
  workspaceId: string;
62
75
  userId: string | null;
63
- kind: "generate" | "run" | "agent";
64
76
  requestedModel: string;
65
77
  virtualModel: boolean;
66
78
  planId: string | null;
@@ -127,6 +139,10 @@ export type Attempt = {
127
139
  httpStatus: number | null;
128
140
  ttfbMs: number | null;
129
141
  ttftMs: number | null;
142
+ /** First reasoning delta to first text delta, or stream end when no text arrives. */
143
+ reasoningMs?: number | null;
144
+ /** First text delta to provider stream end. Null when no text arrives. */
145
+ outputMs?: number | null;
130
146
  startedAt: string;
131
147
  endedAt: string | null;
132
148
  };
@@ -139,28 +155,16 @@ export type PreparedStep = {
139
155
  maxOutputTokens: number;
140
156
  cache: RequestInput["cache"] | null;
141
157
  };
142
- export type CallerReview = {
143
- stage: "after_model" | "after_tools";
144
- stepId: string;
145
- toolCalls?: ToolCall[];
146
- toolResults?: Message["toolResults"];
147
- usage?: Json;
148
- customerCost?: string | null;
149
- supplierCost?: string | null;
150
- customerPrice?: Json;
151
- output?: Json;
152
- };
153
158
  export type PendingInput = {
154
159
  id: string;
155
160
  requestId: string;
156
161
  stepId: string | null;
157
- kind: "tool" | "prepare_step" | "caller_review" | "user_input";
162
+ kind: "tool" | "user_input" | "before_model" | "after_model" | "after_step";
158
163
  payload: {
159
164
  providerCallId?: string;
160
165
  name?: string;
161
166
  arguments?: Json;
162
167
  validationError?: string | null;
163
- stage?: CallerReview["stage"];
164
168
  toolCalls?: ToolCall[];
165
169
  toolResults?: Message["toolResults"];
166
170
  usage?: Json;
@@ -186,6 +190,39 @@ export type Span = {
186
190
  status: "running" | "ok" | "error" | "cancelled";
187
191
  attributes?: Record<string, Json>;
188
192
  };
193
+ export type TimelineRow = {
194
+ id: string;
195
+ requestId: string;
196
+ parentId: string | null;
197
+ kind: "request" | "step" | "attempt" | "input" | "span" | "phase" | "http_phase";
198
+ label: string;
199
+ status: string;
200
+ start: string;
201
+ end: string | null;
202
+ origin: "recorded" | "derived";
203
+ recordedParentId?: string | null;
204
+ recordedStart?: string | null;
205
+ recordedEnd?: string | null;
206
+ derivedFrom?: string[];
207
+ source?: "caller" | "constellation";
208
+ parentSpanId?: string | null;
209
+ inputId?: string | null;
210
+ stepId?: string | null;
211
+ attemptId?: string | null;
212
+ [key: string]: Json | undefined;
213
+ };
214
+ export type Timeline = {
215
+ workspaceId: string;
216
+ externalId: string | null;
217
+ rows: TimelineRow[];
218
+ total: number;
219
+ offset: number;
220
+ limit: number;
221
+ previousRequestId: string | null;
222
+ nextRequestId: string | null;
223
+ truncated: boolean;
224
+ asOf: string;
225
+ };
189
226
  export type Log = {
190
227
  id: string;
191
228
  stepId?: string | null;
@@ -217,10 +254,28 @@ export type InputResponse = {
217
254
  claimId?: string;
218
255
  result?: Json;
219
256
  isError?: boolean;
220
- instructions?: string;
221
- tools?: Tool[];
222
257
  stepInput?: PreparedStep;
223
- action?: "continue" | "refresh_context" | "wait_for_user" | "complete" | "fail";
258
+ action?: "continue" | "wait_for_user" | "wait" | "complete" | "fail";
259
+ };
260
+ export type InputHandlers = {
261
+ checkpoints?: Partial<Record<"beforeModel" | "afterModel" | "afterStep", (payload: PendingInput["payload"], state: Inspection, context: {
262
+ inputId: string;
263
+ parentSpanId: string;
264
+ signal: AbortSignal;
265
+ }) => Promise<InputResponse>>>;
266
+ tools?: Record<string, (args: Json, context: {
267
+ inputId: string;
268
+ requestId: string;
269
+ signal?: AbortSignal;
270
+ }) => Promise<Json>>;
271
+ userInput?: (state: Inspection, context: {
272
+ inputId: string;
273
+ signal: AbortSignal;
274
+ }) => Promise<Json>;
275
+ signal?: AbortSignal;
276
+ consumerId?: string;
277
+ toolTimeoutMs?: number;
278
+ onUpdate?: (state: Inspection) => void;
224
279
  };
225
280
  export type RequestEvent = {
226
281
  seq: number;
@@ -228,6 +283,44 @@ export type RequestEvent = {
228
283
  data: Record<string, Json>;
229
284
  timestamp: string;
230
285
  };
286
+ export type Page<T> = {
287
+ items: T[];
288
+ total: number;
289
+ offset: number;
290
+ limit: number;
291
+ };
292
+ export type PageQuery = {
293
+ offset?: number;
294
+ limit?: number;
295
+ q?: string;
296
+ filters?: string;
297
+ };
298
+ export type ModelScope = {
299
+ workspaceId: string;
300
+ userId?: string;
301
+ planId?: string;
302
+ };
303
+ export type ModelResolution = ModelScope & {
304
+ targetId: string;
305
+ inputTokens?: number;
306
+ outputTokens?: number;
307
+ parameters?: Record<string, Json>;
308
+ };
309
+ export type WebhookEvent = {
310
+ requestId: string;
311
+ inputId: string;
312
+ kind: PendingInput["kind"];
313
+ payload: Record<string, Json>;
314
+ } | {
315
+ requestId: string;
316
+ kind: "request.events";
317
+ afterSeq: number;
318
+ events: Json[];
319
+ } | {
320
+ requestId: string;
321
+ eventSeq: number;
322
+ kind: "request.completed" | "request.failed" | "request.cancelled";
323
+ };
231
324
  export declare class ConstellationError extends Error {
232
325
  status: number;
233
326
  code: string;
@@ -250,6 +343,11 @@ export declare class Constellation {
250
343
  id: string;
251
344
  }>;
252
345
  inspect(id: string, signal?: AbortSignal): Promise<Inspection>;
346
+ /** Canonical API timeline shared with the console. Derived rows disclose their timestamp basis. */
347
+ timeline(id: string, options?: {
348
+ requestId?: string;
349
+ signal?: AbortSignal;
350
+ }): Promise<Timeline>;
253
351
  /** Request context without loading historical telemetry. Fetch records by page. */
254
352
  inspectSummary(id: string, signal?: AbortSignal): Promise<Inspection>;
255
353
  records<K extends keyof RecordTypes>(id: string, kind: K, query?: {
@@ -325,28 +423,189 @@ export declare class Constellation {
325
423
  after?: number;
326
424
  signal?: AbortSignal;
327
425
  }): AsyncGenerator<RequestEvent>;
328
- /** Disconnecting this consumer leaves unresolved inputs durably waiting in Constellation. */
329
- drive(id: string, options?: {
330
- tools?: Record<string, (args: Json, context: {
331
- inputId: string;
332
- requestId: string;
426
+ /** Process one delivered input and exit. An input notification can be retried safely. */
427
+ private handleInput;
428
+ /** Handle pending inputs until the request ends. */
429
+ private processInputs;
430
+ /** Client-safe discovery with an execution key. */
431
+ get models(): {
432
+ list: (scope: ModelScope, signal?: AbortSignal) => Promise<Json>;
433
+ resolve: (input: ModelResolution, signal?: AbortSignal) => Promise<Json>;
434
+ };
435
+ /** Durable requests. create returns an ID; execution continues independently. */
436
+ get requests(): {
437
+ create: (input: RequestInput, signal?: AbortSignal) => Promise<{
438
+ id: string;
439
+ }>;
440
+ preview: (input: RequestInput, signal?: AbortSignal) => Promise<{
441
+ revision: number;
442
+ resolution: Record<string, Json>;
443
+ rules: Record<string, Json>;
444
+ allowed: boolean;
445
+ dispatched: false;
446
+ }>;
447
+ get: (id: string, signal?: AbortSignal) => Promise<Inspection>;
448
+ getSummary: (id: string, signal?: AbortSignal) => Promise<Inspection>;
449
+ list: (query?: Parameters<Constellation["list"]>[0], signal?: AbortSignal) => Promise<{
450
+ items: RequestRecord[];
451
+ total: number;
452
+ offset: number;
453
+ limit: number;
454
+ }>;
455
+ cancel: (id: string, signal?: AbortSignal) => Promise<void>;
456
+ listEvents: (id: string, after?: number, signal?: AbortSignal) => Promise<{
457
+ items: RequestEvent[];
458
+ }>;
459
+ streamEvents: (id: string, options?: {
460
+ after?: number;
333
461
  signal?: AbortSignal;
334
- }) => Promise<Json>>;
335
- prepareStep?: (state: Inspection, context: {
336
- inputId: string;
337
- signal: AbortSignal;
338
- }) => Promise<Pick<InputResponse, "instructions" | "tools" | "stepInput" | "action">>;
339
- callerReview?: (review: CallerReview, state: Inspection, context: {
340
- inputId: string;
341
- signal: AbortSignal;
342
- }) => Promise<Pick<InputResponse, "action" | "result">>;
343
- userInput?: (state: Inspection, context: {
344
- inputId: string;
345
- signal: AbortSignal;
346
- }) => Promise<Json>;
347
- signal?: AbortSignal;
348
- consumerId?: string;
349
- toolTimeoutMs?: number;
350
- onUpdate?: (state: Inspection) => void;
351
- }): Promise<Inspection>;
462
+ }) => AsyncGenerator<RequestEvent, any, any>;
463
+ listRecords: <K extends keyof RecordTypes>(id: string, kind: K, query?: Parameters<Constellation["records"]>[2], signal?: AbortSignal) => Promise<{
464
+ items: RecordTypes[K][];
465
+ total: number;
466
+ offset: number;
467
+ limit: number;
468
+ }>;
469
+ getTimeline: (id: string, options?: {
470
+ requestId?: string;
471
+ signal?: AbortSignal;
472
+ }) => Promise<Timeline>;
473
+ getHttpExchange: (id: string, attemptId: string, signal?: AbortSignal) => Promise<Json>;
474
+ getHttpBody: (id: string, attemptId: string, query: {
475
+ direction: "request" | "response";
476
+ offset?: number;
477
+ limit?: number;
478
+ }, signal?: AbortSignal) => Promise<Json>;
479
+ };
480
+ /** The same handlers work with webhook delivery and explicit polling. */
481
+ get inputs(): {
482
+ handle: (requestId: string, inputId: string, handlers: InputHandlers) => Promise<Inspection>;
483
+ poll: (requestId: string, handlers: InputHandlers) => Promise<Inspection>;
484
+ claim: (requestId: string, inputId: string, claimId: string, signal?: AbortSignal) => Promise<void>;
485
+ respond: (requestId: string, inputId: string, response: InputResponse, signal?: AbortSignal) => Promise<void>;
486
+ };
487
+ get spans(): {
488
+ start: (requestId: string, name: string, options?: Parameters<Constellation["startSpan"]>[2], signal?: AbortSignal) => Promise<{
489
+ id: string;
490
+ end: (options?: {
491
+ endedAt?: string;
492
+ status?: "ok" | "error" | "cancelled";
493
+ attributes?: Record<string, Json>;
494
+ }, signal?: AbortSignal) => Promise<void>;
495
+ }>;
496
+ add: (requestId: string, spans: Span[], signal?: AbortSignal) => Promise<void>;
497
+ };
498
+ get logs(): {
499
+ add: (requestId: string, logs: Log[], signal?: AbortSignal) => Promise<void>;
500
+ };
501
+ /** Configuration endpoints require a management key. */
502
+ get management(): {
503
+ registry: {
504
+ get: (signal?: AbortSignal) => Promise<Json>;
505
+ listRows: (kind: "models" | "providers" | "plans" | "rules", query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
506
+ update: (input: {
507
+ expectedRevision: number;
508
+ catalog: Json;
509
+ }, signal?: AbortSignal) => Promise<Json>;
510
+ applyChanges: (changes: Json[], signal?: AbortSignal) => Promise<Json>;
511
+ listHistory: (kind: string, id: string, query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
512
+ rollback: (kind: string, id: string, input: {
513
+ toVersion: number;
514
+ expectedVersion: number;
515
+ }, signal?: AbortSignal) => Promise<Json>;
516
+ preview: (input: ModelResolution, signal?: AbortSignal) => Promise<Json>;
517
+ testRules: (scope: {
518
+ workspaceId: string;
519
+ userId?: string;
520
+ }, signal?: AbortSignal) => Promise<Json>;
521
+ };
522
+ keys: {
523
+ list: (signal?: AbortSignal) => Promise<Json>;
524
+ listPage: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
525
+ create: (input: Json, signal?: AbortSignal) => Promise<Json>;
526
+ revoke: (id: string, signal?: AbortSignal) => Promise<void>;
527
+ setInputWebhook: (id: string, input: {
528
+ url: string | null;
529
+ token?: string;
530
+ }, signal?: AbortSignal) => Promise<Json>;
531
+ };
532
+ credentials: {
533
+ list: (signal?: AbortSignal) => Promise<Json>;
534
+ listPage: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
535
+ create: (input: Json, signal?: AbortSignal) => Promise<Json>;
536
+ update: (ref: string, input: Json, signal?: AbortSignal) => Promise<void>;
537
+ delete: (ref: string, signal?: AbortSignal) => Promise<void>;
538
+ };
539
+ executionSettings: {
540
+ get: (signal?: AbortSignal) => Promise<Json>;
541
+ update: (input: Json, signal?: AbortSignal) => Promise<Json>;
542
+ listHistory: (signal?: AbortSignal) => Promise<Json>;
543
+ };
544
+ };
545
+ /** Read-only operational endpoints require a telemetry key. */
546
+ get telemetry(): {
547
+ requests: {
548
+ list: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<RequestRecord>>;
549
+ get: (id: string, signal?: AbortSignal) => Promise<Inspection>;
550
+ getSummary: (id: string, signal?: AbortSignal) => Promise<Inspection>;
551
+ listEvents: (id: string, after?: number, signal?: AbortSignal) => Promise<{
552
+ items: RequestEvent[];
553
+ }>;
554
+ listRecords: <K extends keyof RecordTypes>(id: string, kind: K, query?: PageQuery, signal?: AbortSignal) => Promise<Page<RecordTypes[K]>>;
555
+ getTimeline: (id: string, options?: {
556
+ requestId?: string;
557
+ signal?: AbortSignal;
558
+ }) => Promise<Timeline>;
559
+ getHttpExchange: (id: string, attemptId: string, signal?: AbortSignal) => Promise<Json>;
560
+ getHttpBody: (id: string, attemptId: string, query: {
561
+ direction: "request" | "response";
562
+ offset?: number;
563
+ limit?: number;
564
+ }, signal?: AbortSignal) => Promise<Json>;
565
+ };
566
+ system: {
567
+ get: (signal?: AbortSignal) => Promise<Json>;
568
+ listHistory: (query?: {
569
+ hours?: 1 | 24 | 168;
570
+ }, signal?: AbortSignal) => Promise<Json>;
571
+ getComponent: (component: string, signal?: AbortSignal) => Promise<Json>;
572
+ };
573
+ observability: {
574
+ get: (query?: PageQuery, signal?: AbortSignal) => Promise<Json>;
575
+ getSeries: (query: {
576
+ source: "local" | "gateway";
577
+ hours?: 1 | 24 | 168;
578
+ providerId?: string;
579
+ modelId?: string;
580
+ }, signal?: AbortSignal) => Promise<Json>;
581
+ getTiming: (query?: {
582
+ hours?: 1 | 24 | 168;
583
+ }, signal?: AbortSignal) => Promise<Json>;
584
+ };
585
+ jobs: {
586
+ listHistory: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
587
+ };
588
+ notifications: {
589
+ list: (query?: PageQuery & {
590
+ status?: "active" | "resolved";
591
+ }, signal?: AbortSignal) => Promise<Page<Json>>;
592
+ get: (id: string, query?: PageQuery, signal?: AbortSignal) => Promise<Json>;
593
+ listEvents: (query?: {
594
+ after?: number;
595
+ }, signal?: AbortSignal) => Promise<Json>;
596
+ };
597
+ logs: {
598
+ searchSystem: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
599
+ searchRequests: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
600
+ searchAudit: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
601
+ };
602
+ };
603
+ get health(): {
604
+ live: (signal?: AbortSignal) => Promise<Json>;
605
+ ready: (signal?: AbortSignal) => Promise<Json>;
606
+ };
607
+ get webhooks(): {
608
+ verify: (request: Request, expectedToken: string) => Promise<WebhookEvent>;
609
+ };
352
610
  }
611
+ export {};
package/dist/index.js CHANGED
@@ -21,6 +21,13 @@ export function uuid7() {
21
21
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
22
22
  }
23
23
  const terminal = (s) => ["completed", "failed", "cancelled"].includes(s);
24
+ function queryString(values) {
25
+ const params = new URLSearchParams();
26
+ for (const [key, value] of Object.entries(values))
27
+ if (value !== undefined && value !== null)
28
+ params.set(key, String(value));
29
+ return params.size ? `?${params}` : "";
30
+ }
24
31
  async function pause(ms, signal) {
25
32
  signal?.throwIfAborted();
26
33
  await new Promise((resolve, reject) => {
@@ -53,7 +60,10 @@ export class Constellation {
53
60
  this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
54
61
  }
55
62
  url(path) {
56
- return `${this.options.baseUrl.replace(/\/$/, "")}${path.replace(/^\/v1(?=\/)/, this.options.apiPrefix ?? "/v1")}`;
63
+ const route = this.options.apiPrefix === "/internal"
64
+ ? path.replace(/^\/v1(?:\/management|\/telemetry)?(?=\/)/, "/internal")
65
+ : path;
66
+ return `${this.options.baseUrl.replace(/\/$/, "")}${route}`;
57
67
  }
58
68
  async call(path, method = "GET", body, signal) {
59
69
  const response = await this.fetcher(this.url(path), {
@@ -86,6 +96,14 @@ export class Constellation {
86
96
  inspect(id, signal) {
87
97
  return this.call(`/v1/requests/${encodeURIComponent(id)}`, "GET", undefined, signal);
88
98
  }
99
+ /** Canonical API timeline shared with the console. Derived rows disclose their timestamp basis. */
100
+ timeline(id, options = {}) {
101
+ const params = new URLSearchParams();
102
+ if (options.requestId)
103
+ params.set("requestId", options.requestId);
104
+ const query = params.size ? `?${params}` : "";
105
+ return this.call(`/v1/requests/${encodeURIComponent(id)}/timeline${query}`, "GET", undefined, options.signal);
106
+ }
89
107
  /** Request context without loading historical telemetry. Fetch records by page. */
90
108
  inspectSummary(id, signal) {
91
109
  return this.call(`/v1/requests/${encodeURIComponent(id)}?summary=true`, "GET", undefined, signal);
@@ -261,57 +279,35 @@ export class Constellation {
261
279
  await pause(500, options.signal);
262
280
  }
263
281
  }
264
- /** Disconnecting this consumer leaves unresolved inputs durably waiting in Constellation. */
265
- async drive(id, options = {}) {
282
+ /** Process one delivered input and exit. An input notification can be retried safely. */
283
+ async handleInput(id, inputId, options = {}) {
284
+ return this.processInputs(id, { ...options, targetInputId: inputId });
285
+ }
286
+ /** Handle pending inputs until the request ends. */
287
+ async processInputs(id, options = {}) {
266
288
  const consumerId = options.consumerId ?? uuid7();
267
289
  while (true) {
268
290
  const state = await this.inspect(id, options.signal);
269
291
  options.onUpdate?.(state);
270
292
  if (terminal(state.request.status))
271
293
  return state;
272
- for (const pending of state.inputs.filter((i) => !i.resolvedAt)) {
294
+ if (options.targetInputId) {
295
+ const target = state.inputs.find((input) => input.id === options.targetInputId);
296
+ if (!target)
297
+ throw new Error(`Input ${options.targetInputId} is not in request ${id}`);
298
+ if (target.resolvedAt)
299
+ return state;
300
+ }
301
+ let handled = false;
302
+ for (const pending of state.inputs.filter((i) => !i.resolvedAt && (!options.targetInputId || i.id === options.targetInputId))) {
273
303
  if (pending.claimId && pending.claimId !== consumerId &&
274
- !(state.request.kind === "agent" && pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
275
- continue;
276
- const prepareStep = options.prepareStep;
277
- if (pending.kind === "prepare_step" && prepareStep) {
278
- try {
279
- await this.claim(id, pending.id, consumerId, options.signal);
280
- }
281
- catch (error) {
282
- if (error instanceof ConstellationError && error.status === 409)
283
- continue;
284
- throw error;
285
- }
286
- await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
287
- let closed = false;
288
- const preparation = await this.startSpan(id, "caller.prepare_step", { inputId: pending.id }, activeSignal);
289
- try {
290
- const signal = activeSignal
291
- ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
292
- : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
293
- const response = await bounded(prepareStep(state, { inputId: pending.id, signal }), signal);
294
- await preparation.end({}, activeSignal);
295
- closed = true;
296
- if (state.request.kind === "agent")
297
- await this.claim(id, pending.id, consumerId, activeSignal);
298
- await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
299
- }
300
- catch (error) {
301
- if (!activeSignal?.aborted && !closed)
302
- await preparation.end({ status: "error" }, activeSignal).catch(() => { });
303
- throw error;
304
- }
305
- }, options.signal);
304
+ !(pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
306
305
  continue;
307
- }
308
- const callerReview = options.callerReview;
309
- if (pending.kind === "caller_review" && callerReview) {
310
- const stage = pending.payload.stage;
311
- if (!pending.stepId || (stage !== "after_model" && stage !== "after_tools"))
312
- throw new Error("Caller review is missing its step or stage");
313
- const stepId = pending.stepId;
314
- const review = { ...pending.payload, stepId, stage };
306
+ if (pending.kind === "before_model" || pending.kind === "after_model" || pending.kind === "after_step") {
307
+ const name = pending.kind === "before_model" ? "beforeModel" : pending.kind === "after_model" ? "afterModel" : "afterStep";
308
+ const handler = options.checkpoints?.[name];
309
+ if (!handler)
310
+ continue;
315
311
  try {
316
312
  await this.claim(id, pending.id, consumerId, options.signal);
317
313
  }
@@ -321,24 +317,25 @@ export class Constellation {
321
317
  throw error;
322
318
  }
323
319
  await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
324
- const reviewSpan = await this.startSpan(id, "caller.review", { stepId, inputId: pending.id, attributes: { stage } }, activeSignal);
320
+ const span = await this.startSpan(id, `caller.${name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
325
321
  let closed = false;
326
322
  try {
327
323
  const signal = activeSignal
328
324
  ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
329
325
  : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
330
- const response = await bounded(callerReview(review, state, { inputId: pending.id, signal }), signal);
331
- await reviewSpan.end({}, activeSignal);
326
+ const response = await bounded(handler(pending.payload, state, { inputId: pending.id, parentSpanId: span.id, signal }), signal);
327
+ await span.end({}, activeSignal);
332
328
  closed = true;
333
329
  await this.claim(id, pending.id, consumerId, activeSignal);
334
330
  await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
335
331
  }
336
332
  catch (error) {
337
333
  if (!activeSignal?.aborted && !closed)
338
- await reviewSpan.end({ status: "error" }, activeSignal).catch(() => { });
334
+ await span.end({ status: "error" }, activeSignal).catch(() => { });
339
335
  throw error;
340
336
  }
341
337
  }, options.signal);
338
+ handled = true;
342
339
  continue;
343
340
  }
344
341
  const userInput = options.userInput;
@@ -351,15 +348,15 @@ export class Constellation {
351
348
  continue;
352
349
  throw error;
353
350
  }
354
- await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
351
+ await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
355
352
  const signal = activeSignal
356
353
  ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
357
354
  : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
358
355
  const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
359
- if (state.request.kind === "agent")
360
- await this.claim(id, pending.id, consumerId, activeSignal);
356
+ await this.claim(id, pending.id, consumerId, activeSignal);
361
357
  await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
362
358
  }, options.signal);
359
+ handled = true;
363
360
  continue;
364
361
  }
365
362
  if (pending.kind !== "tool")
@@ -375,7 +372,7 @@ export class Constellation {
375
372
  continue;
376
373
  throw error;
377
374
  }
378
- await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
375
+ await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
379
376
  const span = await this.startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
380
377
  let result;
381
378
  let isError = false;
@@ -399,12 +396,168 @@ export class Constellation {
399
396
  result = { error: error instanceof Error ? error.message : "Tool failed" };
400
397
  }
401
398
  await span.end({ status: isError ? "error" : "ok" }, activeSignal);
402
- if (state.request.kind === "agent")
403
- await this.claim(id, pending.id, consumerId, activeSignal);
399
+ await this.claim(id, pending.id, consumerId, activeSignal);
404
400
  await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
405
401
  }, options.signal);
402
+ handled = true;
403
+ }
404
+ if (options.targetInputId) {
405
+ if (!handled)
406
+ throw new Error(`Input ${options.targetInputId} could not be handled`);
407
+ return this.inspect(id, options.signal);
406
408
  }
407
409
  await pause(500, options.signal);
408
410
  }
409
411
  }
412
+ /** Client-safe discovery with an execution key. */
413
+ get models() {
414
+ return {
415
+ list: (scope, signal) => this.call(`/v1/models${queryString(scope)}`, "GET", undefined, signal),
416
+ resolve: (input, signal) => this.call("/v1/registry/resolve", "POST", input, signal),
417
+ };
418
+ }
419
+ /** Durable requests. create returns an ID; execution continues independently. */
420
+ get requests() {
421
+ return {
422
+ create: (input, signal) => this.submit(input, signal),
423
+ preview: (input, signal) => this.resolve(input, signal),
424
+ get: (id, signal) => this.inspect(id, signal),
425
+ getSummary: (id, signal) => this.inspectSummary(id, signal),
426
+ list: (query = {}, signal) => this.list(query, signal),
427
+ cancel: (id, signal) => this.cancel(id, signal),
428
+ listEvents: (id, after = 0, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/events${queryString({ after })}`, "GET", undefined, signal),
429
+ streamEvents: (id, options = {}) => this.events(id, options),
430
+ listRecords: (id, kind, query = {}, signal) => this.records(id, kind, query, signal),
431
+ getTimeline: (id, options = {}) => this.timeline(id, options),
432
+ getHttpExchange: (id, attemptId, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}`, "GET", undefined, signal),
433
+ getHttpBody: (id, attemptId, query, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal),
434
+ };
435
+ }
436
+ /** The same handlers work with webhook delivery and explicit polling. */
437
+ get inputs() {
438
+ return {
439
+ handle: (requestId, inputId, handlers) => this.handleInput(requestId, inputId, handlers),
440
+ poll: (requestId, handlers) => this.processInputs(requestId, handlers),
441
+ claim: (requestId, inputId, claimId, signal) => this.claim(requestId, inputId, claimId, signal),
442
+ respond: (requestId, inputId, response, signal) => this.respond(requestId, inputId, response, signal),
443
+ };
444
+ }
445
+ get spans() {
446
+ return {
447
+ start: (requestId, name, options = {}, signal) => this.startSpan(requestId, name, options, signal),
448
+ add: (requestId, spans, signal) => this.addSpans(requestId, spans, signal),
449
+ };
450
+ }
451
+ get logs() {
452
+ return { add: (requestId, logs, signal) => this.addLogs(requestId, logs, signal) };
453
+ }
454
+ /** Configuration endpoints require a management key. */
455
+ get management() {
456
+ return {
457
+ registry: {
458
+ get: (signal) => this.call("/v1/management/registry", "GET", undefined, signal),
459
+ listRows: (kind, query = {}, signal) => this.call(`/v1/management/registry/list/${kind}${queryString(query)}`, "GET", undefined, signal),
460
+ update: (input, signal) => this.call("/v1/management/registry", "PUT", input, signal),
461
+ applyChanges: (changes, signal) => this.call("/v1/management/registry/changes", "POST", { changes }, signal),
462
+ listHistory: (kind, id, query = {}, signal) => this.call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${queryString(query)}`, "GET", undefined, signal),
463
+ rollback: (kind, id, input, signal) => this.call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/rollback`, "POST", input, signal),
464
+ preview: (input, signal) => this.call("/v1/management/registry/preview", "POST", input, signal),
465
+ testRules: (scope, signal) => this.call("/v1/management/registry/rules/test", "POST", scope, signal),
466
+ },
467
+ keys: {
468
+ list: (signal) => this.call("/v1/management/api-keys", "GET", undefined, signal),
469
+ listPage: (query = {}, signal) => this.call(`/v1/management/api-keys/page${queryString(query)}`, "GET", undefined, signal),
470
+ create: (input, signal) => this.call("/v1/management/api-keys", "POST", input, signal),
471
+ revoke: (id, signal) => this.call(`/v1/management/api-keys/${encodeURIComponent(id)}`, "DELETE", undefined, signal),
472
+ setInputWebhook: (id, input, signal) => this.call(`/v1/management/api-keys/${encodeURIComponent(id)}/input-webhook`, "PUT", input, signal),
473
+ },
474
+ credentials: {
475
+ list: (signal) => this.call("/v1/management/credentials", "GET", undefined, signal),
476
+ listPage: (query = {}, signal) => this.call(`/v1/management/credentials/page${queryString(query)}`, "GET", undefined, signal),
477
+ create: (input, signal) => this.call("/v1/management/credentials", "POST", input, signal),
478
+ update: (ref, input, signal) => this.call(`/v1/management/credentials/${encodeURIComponent(ref)}`, "PUT", input, signal),
479
+ delete: (ref, signal) => this.call(`/v1/management/credentials/${encodeURIComponent(ref)}`, "DELETE", undefined, signal),
480
+ },
481
+ executionSettings: {
482
+ get: (signal) => this.call("/v1/management/execution-settings", "GET", undefined, signal),
483
+ update: (input, signal) => this.call("/v1/management/execution-settings", "PUT", input, signal),
484
+ listHistory: (signal) => this.call("/v1/management/execution-settings/history", "GET", undefined, signal),
485
+ },
486
+ };
487
+ }
488
+ /** Read-only operational endpoints require a telemetry key. */
489
+ get telemetry() {
490
+ const requestPath = (id) => `/v1/telemetry/requests/${encodeURIComponent(id)}`;
491
+ return {
492
+ requests: {
493
+ list: (query = {}, signal) => this.call(`/v1/telemetry/requests${queryString(query)}`, "GET", undefined, signal),
494
+ get: (id, signal) => this.call(requestPath(id), "GET", undefined, signal),
495
+ getSummary: (id, signal) => this.call(`${requestPath(id)}?summary=true`, "GET", undefined, signal),
496
+ listEvents: (id, after = 0, signal) => this.call(`${requestPath(id)}/events${queryString({ after })}`, "GET", undefined, signal),
497
+ listRecords: (id, kind, query = {}, signal) => this.call(`${requestPath(id)}/records/${kind}${queryString(query)}`, "GET", undefined, signal),
498
+ getTimeline: (id, options = {}) => this.call(`${requestPath(id)}/timeline${queryString({ requestId: options.requestId })}`, "GET", undefined, options.signal),
499
+ getHttpExchange: (id, attemptId, signal) => this.call(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}`, "GET", undefined, signal),
500
+ getHttpBody: (id, attemptId, query, signal) => this.call(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal),
501
+ },
502
+ system: {
503
+ get: (signal) => this.call("/v1/telemetry/system", "GET", undefined, signal),
504
+ listHistory: (query = {}, signal) => this.call(`/v1/telemetry/system/history${queryString(query)}`, "GET", undefined, signal),
505
+ getComponent: (component, signal) => this.call(`/v1/telemetry/system/${encodeURIComponent(component)}`, "GET", undefined, signal),
506
+ },
507
+ observability: {
508
+ get: (query = {}, signal) => this.call(`/v1/telemetry/observability${queryString(query)}`, "GET", undefined, signal),
509
+ getSeries: (query, signal) => this.call(`/v1/telemetry/observability/series${queryString(query)}`, "GET", undefined, signal),
510
+ getTiming: (query = {}, signal) => this.call(`/v1/telemetry/observability/timing${queryString(query)}`, "GET", undefined, signal),
511
+ },
512
+ jobs: {
513
+ listHistory: (query = {}, signal) => this.call(`/v1/telemetry/jobs/history${queryString(query)}`, "GET", undefined, signal),
514
+ },
515
+ notifications: {
516
+ list: (query = {}, signal) => this.call(`/v1/telemetry/notifications${queryString(query)}`, "GET", undefined, signal),
517
+ get: (id, query = {}, signal) => this.call(`/v1/telemetry/notifications/${encodeURIComponent(id)}${queryString(query)}`, "GET", undefined, signal),
518
+ listEvents: (query = {}, signal) => this.call(`/v1/telemetry/notifications/events${queryString(query)}`, "GET", undefined, signal),
519
+ },
520
+ logs: {
521
+ searchSystem: (query = {}, signal) => this.call(`/v1/telemetry/logs/system${queryString(query)}`, "GET", undefined, signal),
522
+ searchRequests: (query = {}, signal) => this.call(`/v1/telemetry/logs/requests${queryString(query)}`, "GET", undefined, signal),
523
+ searchAudit: (query = {}, signal) => this.call(`/v1/telemetry/logs/audit${queryString(query)}`, "GET", undefined, signal),
524
+ },
525
+ };
526
+ }
527
+ get health() {
528
+ return {
529
+ live: (signal) => this.call("/healthz", "GET", undefined, signal),
530
+ ready: (signal) => this.call("/readyz", "GET", undefined, signal),
531
+ };
532
+ }
533
+ get webhooks() {
534
+ return {
535
+ verify: async (request, expectedToken) => {
536
+ if (!expectedToken)
537
+ throw new Error("Constellation webhook token is not configured");
538
+ const actual = request.headers.get("x-constellation-webhook-token") ?? "";
539
+ let mismatch = actual.length ^ expectedToken.length;
540
+ for (let i = 0; i < Math.max(actual.length, expectedToken.length); i++)
541
+ mismatch |= (actual.charCodeAt(i) || 0) ^ (expectedToken.charCodeAt(i) || 0);
542
+ if (mismatch !== 0)
543
+ throw new Error("Invalid Constellation webhook token");
544
+ const body = await request.json();
545
+ if (!body || typeof body !== "object")
546
+ throw new Error("Invalid Constellation webhook body");
547
+ const event = body;
548
+ if (typeof event.requestId !== "string" || !event.requestId)
549
+ throw new Error("Invalid Constellation request ID");
550
+ if (typeof event.inputId === "string" && ["tool", "before_model", "after_model", "after_step", "user_input"].includes(String(event.kind))) {
551
+ if (request.headers.get("x-constellation-input-id") !== event.inputId || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload))
552
+ throw new Error("Invalid Constellation input notification");
553
+ return event;
554
+ }
555
+ if (event.kind === "request.events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
556
+ return event;
557
+ if (["request.completed", "request.failed", "request.cancelled"].includes(String(event.kind)) && Number.isSafeInteger(event.eventSeq))
558
+ return event;
559
+ throw new Error("Unknown Constellation webhook event");
560
+ },
561
+ };
562
+ }
410
563
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.1.2",
4
- "description": "TypeScript client for durable model requests and caller-controlled agent execution",
3
+ "version": "0.2.0",
4
+ "description": "TypeScript client for durable model requests and caller events",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "repository": {