@lionhummer/constellation 0.1.2 → 0.1.3

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,172 @@
1
1
  # Constellation TypeScript SDK
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.
3
+ `@lionhummer/constellation` is a server-side client for durable model execution. A request keeps running after `requests.create()` returns its ID. Constellation owns admission, routing, provider calls, waits and the execution trace. Your application supplies conversation state, tools and any policy or billing decisions.
4
4
 
5
- ## Install and connect
6
-
7
- ```bash
5
+ ```sh
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, type InputHandlers } from "@lionhummer/constellation";
15
11
 
16
12
  const client = new Constellation({
17
- baseUrl: "https://constellation.example.com",
18
- apiKey: process.env.CONSTELLATION_API_KEY,
13
+ baseUrl: "https://your-constellation-host.example",
14
+ apiKey: process.env.CONSTELLATION_EXECUTION_KEY!,
19
15
  });
20
16
  ```
21
17
 
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.
18
+ Keep keys on a trusted server. One key may have execution, management and telemetry permissions; the method namespaces below map to those API permissions. An `AbortSignal` stops the current SDK operation, not the durable request. Use `requests.cancel(id)` to cancel execution.
19
+
20
+ ## Choose an execution kind
23
21
 
24
- ## Make a request
22
+ | Kind | Model steps | Caller participation |
23
+ | --- | --- | --- |
24
+ | `generate` | Exactly one. Tools are not accepted. | Observe the result. |
25
+ | `run` | Continues through model steps and caller tools. | Implement tools; there is no review gate before tool dispatch. |
26
+ | `agent` | First step executes from the complete submitted input; later steps use fresh complete inputs. | Review model output and usage before tools, review after tools, and decide whether to continue. |
25
27
 
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.
28
+ All three use `client.requests.create(input)` and return `{ id }`. Persist the UUIDv7 `idempotencyKey` before submitting. If the POST outcome is uncertain, retry with that same key **and identical input**. `externalId` groups separate requests and can hint at prior-provider affinity; it does not retrieve conversation history.
29
+
30
+ ### One-step generation
27
31
 
28
32
  ```ts
29
- const { id } = await client.submit({
33
+ const { id } = await client.requests.create({
34
+ kind: "generate",
30
35
  idempotencyKey: uuid7(),
31
36
  workspaceId: "workspace-123",
32
- externalId: "conversation-456",
33
37
  model: "catalog/example-model",
34
- kind: "generate",
35
- messages: [{ role: "user", text: "Explain the result." }],
38
+ messages: [{ role: "user", text: "Explain this result." }],
36
39
  });
37
40
 
38
- for await (const event of client.events(id)) {
39
- console.log(event.seq, event.type, event.data);
41
+ for await (const event of client.requests.streamEvents(id)) {
42
+ // Store event.seq to resume after a disconnect.
43
+ if (["request.completed", "request.failed", "request.cancelled"].includes(event.type)) break;
40
44
  }
41
-
42
- const final = await client.inspect(id);
43
- console.log(final.request.status, final.request.output);
45
+ const final = await client.requests.get(id);
44
46
  ```
45
47
 
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.
48
+ `requests.preview(input)` applies access, pricing and routing rules without creating or dispatching a request. `models.list({ workspaceId })` discovers accessible models; `models.resolve({ workspaceId, targetId })` previews one model's candidates.
47
49
 
48
- ## Run an agent
50
+ ### Tool-capable run
49
51
 
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.
52
+ Use `run` when tools should execute without an application review gate after each model response. Each tool call becomes a durable caller input. The same handlers work with webhook delivery and explicit polling:
51
53
 
52
54
  ```ts
53
- import type { Message } from "@lionhummer/constellation";
55
+ const handlers = {
56
+ consumerId: "stable-worker-id",
57
+ tools: {
58
+ lookup: async (args, { inputId, signal }) => {
59
+ // Deduplicate external effects by inputId: callbacks can be delivered again.
60
+ return lookupOnce(args, inputId, signal);
61
+ },
62
+ },
63
+ } satisfies InputHandlers;
64
+
65
+ // From one verified input webhook:
66
+ await client.inputs.handle(requestId, inputId, handlers);
67
+
68
+ // Or run a polling consumer until terminal:
69
+ await client.inputs.poll(requestId, handlers);
70
+ ```
71
+
72
+ `inputs.handle()` processes only the named input and returns. `inputs.poll()` repeatedly inspects and handles inputs until the request is terminal. Neither silently switches delivery modes. For custom consumers, `inputs.claim()` and `inputs.respond()` expose the underlying claim and response operations.
54
73
 
55
- const history: Message[] = [{ role: "user", text: "Find the answer." }];
56
- const { id } = await client.submit({
74
+ ### Caller-controlled agent
75
+
76
+ An agent submission contains the **complete first step**. Do not submit a placeholder message expecting an initial preparation callback. The SDK sets the server's `prepareStep` protocol flag for `agent` requests.
77
+
78
+ ```ts
79
+ const { id } = await client.requests.create({
80
+ kind: "agent",
57
81
  idempotencyKey: uuid7(),
58
82
  workspaceId: "workspace-123",
83
+ externalId: "conversation-456",
59
84
  model: "catalog/example-model",
60
- kind: "agent",
61
- maxSteps: 4,
62
- prepareStep: true,
63
- messages: history,
85
+ instructions: "Work carefully and use tools when needed.",
86
+ messages: fullFirstStepHistory,
87
+ tools: toolDefinitions,
88
+ parameters: {},
89
+ maxOutputTokens: 4096,
64
90
  });
91
+ ```
65
92
 
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
- },
93
+ After each model step, Constellation issues `caller_review` with output, usage, customer cost, supplier cost and pending tools. Your application can settle usage and decide whether to allow tools. After tools, another review can finish or request the next step. Before a later model step, `prepare_step` asks for the **whole next input**: model, instructions, messages, tools, parameters, output limit and cache policy.
108
94
 
95
+ ```ts
96
+ const handlers = {
97
+ consumerId: "stable-worker-id",
98
+ callerReview: async (review, state, { inputId }) => ({
99
+ action: await settleAndDecideOnce(review, state, inputId),
100
+ }),
109
101
  tools: {
110
- lookup: async (args, { inputId, signal }) => {
111
- signal?.throwIfAborted();
112
- // Deduplicate external side effects by inputId.
113
- return { answer: JSON.stringify(args) };
114
- },
102
+ lookup: async (args, { inputId, signal }) => lookupOnce(args, inputId, signal),
115
103
  },
116
- });
117
-
118
- console.log(final.request.status, final.steps);
104
+ prepareStep: async (state, { signal, parentSpanId }) => ({
105
+ stepInput: await buildCompleteNextStep(state, { signal, parentSpanId }),
106
+ }),
107
+ } satisfies InputHandlers;
119
108
  ```
120
109
 
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.
110
+ Deduplicate billing and tool side effects by input ID. The SDK claims an input, renews an agent claim while its handler runs, adds parent spans for review/preparation/tool callbacks, and responds. If handling fails transiently, return a non-2xx webhook response so delivery retries. A terminal request rejects new caller spans and logs.
122
111
 
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.
112
+ ## Webhooks and events
124
113
 
125
- ## Record caller work
126
-
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:
114
+ Configure the execution key's HTTPS `inputWebhook` URL and random token when creating the key, or later with `management.keys.setInputWebhook()`. The SDK can verify the token and parse the three callback shapes:
128
115
 
129
116
  ```ts
130
- const span = await client.startSpan(id, "application.context_lookup", {
131
- inputId: "pending-input-id",
132
- startedAt: new Date().toISOString(),
133
- });
117
+ const event = await client.webhooks.verify(request, process.env.CONSTELLATION_WEBHOOK_TOKEN!);
118
+ if ("inputId" in event) {
119
+ await client.inputs.handle(event.requestId, event.inputId, handlers);
120
+ } else if (event.kind === "request.model_events") {
121
+ await storeDeltaBatchOnce(event.requestId, event.afterSeq, event.events);
122
+ } else {
123
+ const state = await client.requests.getSummary(event.requestId);
124
+ await finalizeOnce(event.requestId, event.eventSeq, state);
125
+ }
126
+ // Return 2xx after your input response or other effects are durable.
127
+ ```
128
+
129
+ Input callbacks include `{requestId,inputId,kind,payload}` and `x-constellation-input-id`. Model-event callbacks contain `{requestId,kind:"request.model_events",afterSeq,events}`. Terminal callbacks contain `{requestId,eventSeq,kind}`. All carry `x-constellation-webhook-token`. Delivery is at least once. Model deltas may arrive late; use the final inspection as authority after terminal.
134
130
 
135
- // Perform the work and close the span before resolving the wait.
136
- await span.end({ status: "ok" });
131
+ `requests.streamEvents(id, { after, signal })` is SSE with durable sequence replay. `requests.listEvents(id, after)` reads a bounded event page. SSE observes output; it does not process caller inputs. A dropped stream reconnects by cursor and does not cancel the request.
132
+
133
+ ## Inspect and instrument
134
+
135
+ ```ts
136
+ const summary = await client.requests.getSummary(id);
137
+ const steps = await client.requests.listRecords(id, "steps", { limit: 25, offset: 0 });
138
+ const timeline = await client.requests.getTimeline(id);
139
+
140
+ const span = await client.spans.start(id, "application.context", { inputId });
141
+ try {
142
+ await rebuildContext();
143
+ await span.end();
144
+ } catch (error) {
145
+ await span.end({ status: "error" });
146
+ throw error;
147
+ }
137
148
  ```
138
149
 
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.
150
+ `requests.get(id)` loads full inspection. For long runs, use `getSummary()` and `listRecords()` for steps, attempts, inputs, spans or logs. `getTimeline()` returns the same recorded and derived rows as the console. `getHttpExchange()` and `getHttpBody()` inspect a provider attempt. Caller spans may link to an `inputId`, `stepId`, `attemptId` or `parentSpanId`; `client.spans.add()` and `client.logs.add()` write batches.
151
+
152
+ User messages may contain HTTPS image or PDF URL parts. Constellation stores URL references, not media bytes. Provider adapters forward supported URLs or download within limits immediately before dispatch. Keep signed URLs valid until the provider fetches them; refresh URLs in a later agent `prepareStep` callback.
140
153
 
141
- ## Inspect and recover
154
+ ## Other namespaces
142
155
 
143
- | Method | Purpose |
156
+ | Namespace | Methods |
144
157
  | --- | --- |
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. |
154
-
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.
158
+ | `management.registry` | `get`, `listRows`, `update`, `applyChanges`, `listHistory`, `rollback`, `preview`, `testRules` |
159
+ | `management.keys` | `list`, `listPage`, `create`, `revoke`, `setInputWebhook` |
160
+ | `management.credentials` | `list`, `listPage`, `create`, `update`, `delete` |
161
+ | `management.executionSettings` | `get`, `update`, `listHistory` |
162
+ | `telemetry.requests` | `list`, `get`, `getSummary`, `listEvents`, `listRecords`, `getTimeline`, `getHttpExchange`, `getHttpBody` |
163
+ | `telemetry.system` | `get`, `listHistory`, `getComponent` |
164
+ | `telemetry.observability` | `get`, `getSeries`, `getTiming` |
165
+ | `telemetry.jobs` | `listHistory` |
166
+ | `telemetry.notifications` | `list`, `get`, `listEvents` |
167
+ | `telemetry.logs` | `searchSystem`, `searchRequests`, `searchAudit` |
168
+ | `health` | `live`, `ready` (public probes) |
169
+
170
+ Top-level `logs.add()` writes caller logs on an execution key. `telemetry.logs` searches operational logs with a telemetry key. The API's exact parameter and response schemas are served at `/v1/openapi.json` and shown inline in the console's API reference.
171
+
172
+ The client defaults to a 30-second HTTP timeout; configure `timeoutMs` or pass an `AbortSignal`. `ConstellationError` carries HTTP status, server code and message. Older flat method names remain as temporary aliases for existing callers; new integrations should use the names above.
package/dist/index.d.ts CHANGED
@@ -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,6 +56,18 @@ export type RequestInput = {
54
56
  isolationKey?: string;
55
57
  };
56
58
  };
59
+ /** An agent always supplies its first complete step at admission. Later steps use prepare_step. */
60
+ export type RequestInput = RequestFields & ({
61
+ kind: "generate";
62
+ tools?: never;
63
+ prepareStep?: boolean;
64
+ } | {
65
+ kind: "run";
66
+ prepareStep?: boolean;
67
+ } | {
68
+ kind: "agent";
69
+ prepareStep?: true;
70
+ });
57
71
  export type Status = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled";
58
72
  export type RequestRecord = {
59
73
  id: string;
@@ -127,6 +141,10 @@ export type Attempt = {
127
141
  httpStatus: number | null;
128
142
  ttfbMs: number | null;
129
143
  ttftMs: number | null;
144
+ /** First reasoning delta to first text delta, or stream end when no text arrives. */
145
+ reasoningMs?: number | null;
146
+ /** First text delta to provider stream end. Null when no text arrives. */
147
+ outputMs?: number | null;
130
148
  startedAt: string;
131
149
  endedAt: string | null;
132
150
  };
@@ -186,6 +204,39 @@ export type Span = {
186
204
  status: "running" | "ok" | "error" | "cancelled";
187
205
  attributes?: Record<string, Json>;
188
206
  };
207
+ export type TimelineRow = {
208
+ id: string;
209
+ requestId: string;
210
+ parentId: string | null;
211
+ kind: "request" | "step" | "attempt" | "input" | "span" | "phase" | "http_phase";
212
+ label: string;
213
+ status: string;
214
+ start: string;
215
+ end: string | null;
216
+ origin: "recorded" | "derived";
217
+ recordedParentId?: string | null;
218
+ recordedStart?: string | null;
219
+ recordedEnd?: string | null;
220
+ derivedFrom?: string[];
221
+ source?: "caller" | "constellation";
222
+ parentSpanId?: string | null;
223
+ inputId?: string | null;
224
+ stepId?: string | null;
225
+ attemptId?: string | null;
226
+ [key: string]: Json | undefined;
227
+ };
228
+ export type Timeline = {
229
+ workspaceId: string;
230
+ externalId: string | null;
231
+ rows: TimelineRow[];
232
+ total: number;
233
+ offset: number;
234
+ limit: number;
235
+ previousRequestId: string | null;
236
+ nextRequestId: string | null;
237
+ truncated: boolean;
238
+ asOf: string;
239
+ };
189
240
  export type Log = {
190
241
  id: string;
191
242
  stepId?: string | null;
@@ -222,12 +273,76 @@ export type InputResponse = {
222
273
  stepInput?: PreparedStep;
223
274
  action?: "continue" | "refresh_context" | "wait_for_user" | "complete" | "fail";
224
275
  };
276
+ export type InputHandlers = {
277
+ tools?: Record<string, (args: Json, context: {
278
+ inputId: string;
279
+ requestId: string;
280
+ signal?: AbortSignal;
281
+ }) => Promise<Json>>;
282
+ prepareStep?: (state: Inspection, context: {
283
+ inputId: string;
284
+ parentSpanId: string;
285
+ signal: AbortSignal;
286
+ }) => Promise<Pick<InputResponse, "instructions" | "tools" | "stepInput" | "action">>;
287
+ callerReview?: (review: CallerReview, state: Inspection, context: {
288
+ inputId: string;
289
+ signal: AbortSignal;
290
+ }) => Promise<Pick<InputResponse, "action" | "result">>;
291
+ userInput?: (state: Inspection, context: {
292
+ inputId: string;
293
+ signal: AbortSignal;
294
+ }) => Promise<Json>;
295
+ signal?: AbortSignal;
296
+ consumerId?: string;
297
+ toolTimeoutMs?: number;
298
+ onUpdate?: (state: Inspection) => void;
299
+ };
300
+ /** @deprecated Use InputHandlers. */
301
+ export type DriveOptions = InputHandlers;
225
302
  export type RequestEvent = {
226
303
  seq: number;
227
304
  type: string;
228
305
  data: Record<string, Json>;
229
306
  timestamp: string;
230
307
  };
308
+ export type Page<T> = {
309
+ items: T[];
310
+ total: number;
311
+ offset: number;
312
+ limit: number;
313
+ };
314
+ export type PageQuery = {
315
+ offset?: number;
316
+ limit?: number;
317
+ q?: string;
318
+ filters?: string;
319
+ };
320
+ export type ModelScope = {
321
+ workspaceId: string;
322
+ userId?: string;
323
+ planId?: string;
324
+ };
325
+ export type ModelResolution = ModelScope & {
326
+ targetId: string;
327
+ inputTokens?: number;
328
+ outputTokens?: number;
329
+ parameters?: Record<string, Json>;
330
+ };
331
+ export type WebhookEvent = {
332
+ requestId: string;
333
+ inputId: string;
334
+ kind: "tool" | "prepare_step" | "caller_review" | "user_input";
335
+ payload: Record<string, Json>;
336
+ } | {
337
+ requestId: string;
338
+ kind: "request.model_events";
339
+ afterSeq: number;
340
+ events: Json[];
341
+ } | {
342
+ requestId: string;
343
+ eventSeq: number;
344
+ kind: "request.completed" | "request.failed" | "request.cancelled";
345
+ };
231
346
  export declare class ConstellationError extends Error {
232
347
  status: number;
233
348
  code: string;
@@ -250,6 +365,11 @@ export declare class Constellation {
250
365
  id: string;
251
366
  }>;
252
367
  inspect(id: string, signal?: AbortSignal): Promise<Inspection>;
368
+ /** Canonical API timeline shared with the console. Derived rows disclose their timestamp basis. */
369
+ timeline(id: string, options?: {
370
+ requestId?: string;
371
+ signal?: AbortSignal;
372
+ }): Promise<Timeline>;
253
373
  /** Request context without loading historical telemetry. Fetch records by page. */
254
374
  inspectSummary(id: string, signal?: AbortSignal): Promise<Inspection>;
255
375
  records<K extends keyof RecordTypes>(id: string, kind: K, query?: {
@@ -325,28 +445,191 @@ export declare class Constellation {
325
445
  after?: number;
326
446
  signal?: AbortSignal;
327
447
  }): 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;
333
- 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;
448
+ /** Process one delivered input and exit. An input notification can be retried safely. */
449
+ handleInput(id: string, inputId: string, options?: DriveOptions): Promise<Inspection>;
450
+ /** Legacy long-running helper. Use handleInput from a durable notification handler. */
451
+ drive(id: string, options?: DriveOptions & {
452
+ targetInputId?: string;
351
453
  }): Promise<Inspection>;
454
+ /** Client-safe discovery with an execution key. */
455
+ get models(): {
456
+ list: (scope: ModelScope, signal?: AbortSignal) => Promise<Json>;
457
+ resolve: (input: ModelResolution, signal?: AbortSignal) => Promise<Json>;
458
+ };
459
+ /** Durable requests. create returns an ID; execution continues independently. */
460
+ get requests(): {
461
+ create: (input: RequestInput, signal?: AbortSignal) => Promise<{
462
+ id: string;
463
+ }>;
464
+ preview: (input: RequestInput, signal?: AbortSignal) => Promise<{
465
+ revision: number;
466
+ resolution: Record<string, Json>;
467
+ rules: Record<string, Json>;
468
+ allowed: boolean;
469
+ dispatched: false;
470
+ }>;
471
+ get: (id: string, signal?: AbortSignal) => Promise<Inspection>;
472
+ getSummary: (id: string, signal?: AbortSignal) => Promise<Inspection>;
473
+ list: (query?: Parameters<Constellation["list"]>[0], signal?: AbortSignal) => Promise<{
474
+ items: RequestRecord[];
475
+ total: number;
476
+ offset: number;
477
+ limit: number;
478
+ }>;
479
+ cancel: (id: string, signal?: AbortSignal) => Promise<void>;
480
+ listEvents: (id: string, after?: number, signal?: AbortSignal) => Promise<{
481
+ items: RequestEvent[];
482
+ }>;
483
+ streamEvents: (id: string, options?: {
484
+ after?: number;
485
+ signal?: AbortSignal;
486
+ }) => AsyncGenerator<RequestEvent, any, any>;
487
+ listRecords: <K extends keyof RecordTypes>(id: string, kind: K, query?: Parameters<Constellation["records"]>[2], signal?: AbortSignal) => Promise<{
488
+ items: RecordTypes[K][];
489
+ total: number;
490
+ offset: number;
491
+ limit: number;
492
+ }>;
493
+ getTimeline: (id: string, options?: {
494
+ requestId?: string;
495
+ signal?: AbortSignal;
496
+ }) => Promise<Timeline>;
497
+ getHttpExchange: (id: string, attemptId: string, signal?: AbortSignal) => Promise<Json>;
498
+ getHttpBody: (id: string, attemptId: string, query: {
499
+ direction: "request" | "response";
500
+ offset?: number;
501
+ limit?: number;
502
+ }, signal?: AbortSignal) => Promise<Json>;
503
+ };
504
+ /** The same handlers work with webhook delivery and explicit polling. */
505
+ get inputs(): {
506
+ handle: (requestId: string, inputId: string, handlers: InputHandlers) => Promise<Inspection>;
507
+ poll: (requestId: string, handlers: InputHandlers) => Promise<Inspection>;
508
+ claim: (requestId: string, inputId: string, claimId: string, signal?: AbortSignal) => Promise<void>;
509
+ respond: (requestId: string, inputId: string, response: InputResponse, signal?: AbortSignal) => Promise<void>;
510
+ };
511
+ get spans(): {
512
+ start: (requestId: string, name: string, options?: Parameters<Constellation["startSpan"]>[2], signal?: AbortSignal) => Promise<{
513
+ id: string;
514
+ end: (options?: {
515
+ endedAt?: string;
516
+ status?: "ok" | "error" | "cancelled";
517
+ attributes?: Record<string, Json>;
518
+ }, signal?: AbortSignal) => Promise<void>;
519
+ }>;
520
+ add: (requestId: string, spans: Span[], signal?: AbortSignal) => Promise<void>;
521
+ };
522
+ get logs(): {
523
+ add: (requestId: string, logs: Log[], signal?: AbortSignal) => Promise<void>;
524
+ };
525
+ /** Configuration endpoints require a management key. */
526
+ get management(): {
527
+ registry: {
528
+ get: (signal?: AbortSignal) => Promise<Json>;
529
+ listRows: (kind: "models" | "providers" | "plans" | "rules", query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
530
+ update: (input: {
531
+ expectedRevision: number;
532
+ catalog: Json;
533
+ }, signal?: AbortSignal) => Promise<Json>;
534
+ applyChanges: (changes: Json[], signal?: AbortSignal) => Promise<Json>;
535
+ listHistory: (kind: string, id: string, query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
536
+ rollback: (kind: string, id: string, input: {
537
+ toVersion: number;
538
+ expectedVersion: number;
539
+ }, signal?: AbortSignal) => Promise<Json>;
540
+ preview: (input: ModelResolution, signal?: AbortSignal) => Promise<Json>;
541
+ testRules: (scope: {
542
+ workspaceId: string;
543
+ userId?: string;
544
+ }, signal?: AbortSignal) => Promise<Json>;
545
+ };
546
+ keys: {
547
+ list: (signal?: AbortSignal) => Promise<Json>;
548
+ listPage: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
549
+ create: (input: Json, signal?: AbortSignal) => Promise<Json>;
550
+ revoke: (id: string, signal?: AbortSignal) => Promise<void>;
551
+ setInputWebhook: (id: string, input: {
552
+ url: string | null;
553
+ token?: string;
554
+ }, signal?: AbortSignal) => Promise<Json>;
555
+ };
556
+ credentials: {
557
+ list: (signal?: AbortSignal) => Promise<Json>;
558
+ listPage: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
559
+ create: (input: Json, signal?: AbortSignal) => Promise<Json>;
560
+ update: (ref: string, input: Json, signal?: AbortSignal) => Promise<void>;
561
+ delete: (ref: string, signal?: AbortSignal) => Promise<void>;
562
+ };
563
+ executionSettings: {
564
+ get: (signal?: AbortSignal) => Promise<Json>;
565
+ update: (input: Json, signal?: AbortSignal) => Promise<Json>;
566
+ listHistory: (signal?: AbortSignal) => Promise<Json>;
567
+ };
568
+ };
569
+ /** Read-only operational endpoints require a telemetry key. */
570
+ get telemetry(): {
571
+ requests: {
572
+ list: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<RequestRecord>>;
573
+ get: (id: string, signal?: AbortSignal) => Promise<Inspection>;
574
+ getSummary: (id: string, signal?: AbortSignal) => Promise<Inspection>;
575
+ listEvents: (id: string, after?: number, signal?: AbortSignal) => Promise<{
576
+ items: RequestEvent[];
577
+ }>;
578
+ listRecords: <K extends keyof RecordTypes>(id: string, kind: K, query?: PageQuery, signal?: AbortSignal) => Promise<Page<RecordTypes[K]>>;
579
+ getTimeline: (id: string, options?: {
580
+ requestId?: string;
581
+ signal?: AbortSignal;
582
+ }) => Promise<Timeline>;
583
+ getHttpExchange: (id: string, attemptId: string, signal?: AbortSignal) => Promise<Json>;
584
+ getHttpBody: (id: string, attemptId: string, query: {
585
+ direction: "request" | "response";
586
+ offset?: number;
587
+ limit?: number;
588
+ }, signal?: AbortSignal) => Promise<Json>;
589
+ };
590
+ system: {
591
+ get: (signal?: AbortSignal) => Promise<Json>;
592
+ listHistory: (query?: {
593
+ hours?: 1 | 24 | 168;
594
+ }, signal?: AbortSignal) => Promise<Json>;
595
+ getComponent: (component: string, signal?: AbortSignal) => Promise<Json>;
596
+ };
597
+ observability: {
598
+ get: (query?: PageQuery, signal?: AbortSignal) => Promise<Json>;
599
+ getSeries: (query: {
600
+ source: "local" | "gateway";
601
+ hours?: 1 | 24 | 168;
602
+ providerId?: string;
603
+ modelId?: string;
604
+ }, signal?: AbortSignal) => Promise<Json>;
605
+ getTiming: (query?: {
606
+ hours?: 1 | 24 | 168;
607
+ }, signal?: AbortSignal) => Promise<Json>;
608
+ };
609
+ jobs: {
610
+ listHistory: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
611
+ };
612
+ notifications: {
613
+ list: (query?: PageQuery & {
614
+ status?: "active" | "resolved";
615
+ }, signal?: AbortSignal) => Promise<Page<Json>>;
616
+ get: (id: string, query?: PageQuery, signal?: AbortSignal) => Promise<Json>;
617
+ listEvents: (query?: {
618
+ after?: number;
619
+ }, signal?: AbortSignal) => Promise<Json>;
620
+ };
621
+ logs: {
622
+ searchSystem: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
623
+ searchRequests: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
624
+ searchAudit: (query?: PageQuery, signal?: AbortSignal) => Promise<Page<Json>>;
625
+ };
626
+ };
627
+ get health(): {
628
+ live: (signal?: AbortSignal) => Promise<Json>;
629
+ ready: (signal?: AbortSignal) => Promise<Json>;
630
+ };
631
+ get webhooks(): {
632
+ verify: (request: Request, expectedToken: string) => Promise<WebhookEvent>;
633
+ };
352
634
  }
635
+ 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), {
@@ -81,11 +91,19 @@ export class Constellation {
81
91
  : response.json();
82
92
  }
83
93
  submit(input, signal) {
84
- return this.call("/v1/requests", "POST", input, signal);
94
+ return this.call("/v1/requests", "POST", input.kind === "agent" ? { ...input, prepareStep: true } : input, signal);
85
95
  }
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);
@@ -98,7 +116,7 @@ export class Constellation {
98
116
  return this.call(`/v1/requests/${encodeURIComponent(id)}/records/${kind}?${params}`, "GET", undefined, signal);
99
117
  }
100
118
  resolve(input, signal) {
101
- return this.call("/v1/requests/resolve", "POST", input, signal);
119
+ return this.call("/v1/requests/resolve", "POST", input.kind === "agent" ? { ...input, prepareStep: true } : input, signal);
102
120
  }
103
121
  list(query = {}, signal) {
104
122
  const params = new URLSearchParams();
@@ -261,7 +279,11 @@ 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. */
282
+ /** Process one delivered input and exit. An input notification can be retried safely. */
283
+ async handleInput(id, inputId, options = {}) {
284
+ return this.drive(id, { ...options, targetInputId: inputId });
285
+ }
286
+ /** Legacy long-running helper. Use handleInput from a durable notification handler. */
265
287
  async drive(id, options = {}) {
266
288
  const consumerId = options.consumerId ?? uuid7();
267
289
  while (true) {
@@ -269,7 +291,15 @@ export class Constellation {
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
304
  !(state.request.kind === "agent" && pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
275
305
  continue;
@@ -290,7 +320,7 @@ export class Constellation {
290
320
  const signal = activeSignal
291
321
  ? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
292
322
  : AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
293
- const response = await bounded(prepareStep(state, { inputId: pending.id, signal }), signal);
323
+ const response = await bounded(prepareStep(state, { inputId: pending.id, parentSpanId: preparation.id, signal }), signal);
294
324
  await preparation.end({}, activeSignal);
295
325
  closed = true;
296
326
  if (state.request.kind === "agent")
@@ -303,6 +333,7 @@ export class Constellation {
303
333
  throw error;
304
334
  }
305
335
  }, options.signal);
336
+ handled = true;
306
337
  continue;
307
338
  }
308
339
  const callerReview = options.callerReview;
@@ -339,6 +370,7 @@ export class Constellation {
339
370
  throw error;
340
371
  }
341
372
  }, options.signal);
373
+ handled = true;
342
374
  continue;
343
375
  }
344
376
  const userInput = options.userInput;
@@ -360,6 +392,7 @@ export class Constellation {
360
392
  await this.claim(id, pending.id, consumerId, activeSignal);
361
393
  await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
362
394
  }, options.signal);
395
+ handled = true;
363
396
  continue;
364
397
  }
365
398
  if (pending.kind !== "tool")
@@ -403,8 +436,165 @@ export class Constellation {
403
436
  await this.claim(id, pending.id, consumerId, activeSignal);
404
437
  await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
405
438
  }, options.signal);
439
+ handled = true;
440
+ }
441
+ if (options.targetInputId) {
442
+ if (!handled)
443
+ throw new Error(`Input ${options.targetInputId} could not be handled`);
444
+ return this.inspect(id, options.signal);
406
445
  }
407
446
  await pause(500, options.signal);
408
447
  }
409
448
  }
449
+ /** Client-safe discovery with an execution key. */
450
+ get models() {
451
+ return {
452
+ list: (scope, signal) => this.call(`/v1/models${queryString(scope)}`, "GET", undefined, signal),
453
+ resolve: (input, signal) => this.call("/v1/registry/resolve", "POST", input, signal),
454
+ };
455
+ }
456
+ /** Durable requests. create returns an ID; execution continues independently. */
457
+ get requests() {
458
+ return {
459
+ create: (input, signal) => this.submit(input, signal),
460
+ preview: (input, signal) => this.resolve(input, signal),
461
+ get: (id, signal) => this.inspect(id, signal),
462
+ getSummary: (id, signal) => this.inspectSummary(id, signal),
463
+ list: (query = {}, signal) => this.list(query, signal),
464
+ cancel: (id, signal) => this.cancel(id, signal),
465
+ listEvents: (id, after = 0, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/events${queryString({ after })}`, "GET", undefined, signal),
466
+ streamEvents: (id, options = {}) => this.events(id, options),
467
+ listRecords: (id, kind, query = {}, signal) => this.records(id, kind, query, signal),
468
+ getTimeline: (id, options = {}) => this.timeline(id, options),
469
+ getHttpExchange: (id, attemptId, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}`, "GET", undefined, signal),
470
+ getHttpBody: (id, attemptId, query, signal) => this.call(`/v1/requests/${encodeURIComponent(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal),
471
+ };
472
+ }
473
+ /** The same handlers work with webhook delivery and explicit polling. */
474
+ get inputs() {
475
+ return {
476
+ handle: (requestId, inputId, handlers) => this.handleInput(requestId, inputId, handlers),
477
+ poll: (requestId, handlers) => this.drive(requestId, handlers),
478
+ claim: (requestId, inputId, claimId, signal) => this.claim(requestId, inputId, claimId, signal),
479
+ respond: (requestId, inputId, response, signal) => this.respond(requestId, inputId, response, signal),
480
+ };
481
+ }
482
+ get spans() {
483
+ return {
484
+ start: (requestId, name, options = {}, signal) => this.startSpan(requestId, name, options, signal),
485
+ add: (requestId, spans, signal) => this.addSpans(requestId, spans, signal),
486
+ };
487
+ }
488
+ get logs() {
489
+ return { add: (requestId, logs, signal) => this.addLogs(requestId, logs, signal) };
490
+ }
491
+ /** Configuration endpoints require a management key. */
492
+ get management() {
493
+ return {
494
+ registry: {
495
+ get: (signal) => this.call("/v1/management/registry", "GET", undefined, signal),
496
+ listRows: (kind, query = {}, signal) => this.call(`/v1/management/registry/list/${kind}${queryString(query)}`, "GET", undefined, signal),
497
+ update: (input, signal) => this.call("/v1/management/registry", "PUT", input, signal),
498
+ applyChanges: (changes, signal) => this.call("/v1/management/registry/changes", "POST", { changes }, signal),
499
+ listHistory: (kind, id, query = {}, signal) => this.call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${queryString(query)}`, "GET", undefined, signal),
500
+ rollback: (kind, id, input, signal) => this.call(`/v1/management/registry/history/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/rollback`, "POST", input, signal),
501
+ preview: (input, signal) => this.call("/v1/management/registry/preview", "POST", input, signal),
502
+ testRules: (scope, signal) => this.call("/v1/management/registry/rules/test", "POST", scope, signal),
503
+ },
504
+ keys: {
505
+ list: (signal) => this.call("/v1/management/api-keys", "GET", undefined, signal),
506
+ listPage: (query = {}, signal) => this.call(`/v1/management/api-keys/page${queryString(query)}`, "GET", undefined, signal),
507
+ create: (input, signal) => this.call("/v1/management/api-keys", "POST", input, signal),
508
+ revoke: (id, signal) => this.call(`/v1/management/api-keys/${encodeURIComponent(id)}`, "DELETE", undefined, signal),
509
+ setInputWebhook: (id, input, signal) => this.call(`/v1/management/api-keys/${encodeURIComponent(id)}/input-webhook`, "PUT", input, signal),
510
+ },
511
+ credentials: {
512
+ list: (signal) => this.call("/v1/management/credentials", "GET", undefined, signal),
513
+ listPage: (query = {}, signal) => this.call(`/v1/management/credentials/page${queryString(query)}`, "GET", undefined, signal),
514
+ create: (input, signal) => this.call("/v1/management/credentials", "POST", input, signal),
515
+ update: (ref, input, signal) => this.call(`/v1/management/credentials/${encodeURIComponent(ref)}`, "PUT", input, signal),
516
+ delete: (ref, signal) => this.call(`/v1/management/credentials/${encodeURIComponent(ref)}`, "DELETE", undefined, signal),
517
+ },
518
+ executionSettings: {
519
+ get: (signal) => this.call("/v1/management/execution-settings", "GET", undefined, signal),
520
+ update: (input, signal) => this.call("/v1/management/execution-settings", "PUT", input, signal),
521
+ listHistory: (signal) => this.call("/v1/management/execution-settings/history", "GET", undefined, signal),
522
+ },
523
+ };
524
+ }
525
+ /** Read-only operational endpoints require a telemetry key. */
526
+ get telemetry() {
527
+ const requestPath = (id) => `/v1/telemetry/requests/${encodeURIComponent(id)}`;
528
+ return {
529
+ requests: {
530
+ list: (query = {}, signal) => this.call(`/v1/telemetry/requests${queryString(query)}`, "GET", undefined, signal),
531
+ get: (id, signal) => this.call(requestPath(id), "GET", undefined, signal),
532
+ getSummary: (id, signal) => this.call(`${requestPath(id)}?summary=true`, "GET", undefined, signal),
533
+ listEvents: (id, after = 0, signal) => this.call(`${requestPath(id)}/events${queryString({ after })}`, "GET", undefined, signal),
534
+ listRecords: (id, kind, query = {}, signal) => this.call(`${requestPath(id)}/records/${kind}${queryString(query)}`, "GET", undefined, signal),
535
+ getTimeline: (id, options = {}) => this.call(`${requestPath(id)}/timeline${queryString({ requestId: options.requestId })}`, "GET", undefined, options.signal),
536
+ getHttpExchange: (id, attemptId, signal) => this.call(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}`, "GET", undefined, signal),
537
+ getHttpBody: (id, attemptId, query, signal) => this.call(`${requestPath(id)}/http/${encodeURIComponent(attemptId)}/body${queryString(query)}`, "GET", undefined, signal),
538
+ },
539
+ system: {
540
+ get: (signal) => this.call("/v1/telemetry/system", "GET", undefined, signal),
541
+ listHistory: (query = {}, signal) => this.call(`/v1/telemetry/system/history${queryString(query)}`, "GET", undefined, signal),
542
+ getComponent: (component, signal) => this.call(`/v1/telemetry/system/${encodeURIComponent(component)}`, "GET", undefined, signal),
543
+ },
544
+ observability: {
545
+ get: (query = {}, signal) => this.call(`/v1/telemetry/observability${queryString(query)}`, "GET", undefined, signal),
546
+ getSeries: (query, signal) => this.call(`/v1/telemetry/observability/series${queryString(query)}`, "GET", undefined, signal),
547
+ getTiming: (query = {}, signal) => this.call(`/v1/telemetry/observability/timing${queryString(query)}`, "GET", undefined, signal),
548
+ },
549
+ jobs: {
550
+ listHistory: (query = {}, signal) => this.call(`/v1/telemetry/jobs/history${queryString(query)}`, "GET", undefined, signal),
551
+ },
552
+ notifications: {
553
+ list: (query = {}, signal) => this.call(`/v1/telemetry/notifications${queryString(query)}`, "GET", undefined, signal),
554
+ get: (id, query = {}, signal) => this.call(`/v1/telemetry/notifications/${encodeURIComponent(id)}${queryString(query)}`, "GET", undefined, signal),
555
+ listEvents: (query = {}, signal) => this.call(`/v1/telemetry/notifications/events${queryString(query)}`, "GET", undefined, signal),
556
+ },
557
+ logs: {
558
+ searchSystem: (query = {}, signal) => this.call(`/v1/telemetry/logs/system${queryString(query)}`, "GET", undefined, signal),
559
+ searchRequests: (query = {}, signal) => this.call(`/v1/telemetry/logs/requests${queryString(query)}`, "GET", undefined, signal),
560
+ searchAudit: (query = {}, signal) => this.call(`/v1/telemetry/logs/audit${queryString(query)}`, "GET", undefined, signal),
561
+ },
562
+ };
563
+ }
564
+ get health() {
565
+ return {
566
+ live: (signal) => this.call("/healthz", "GET", undefined, signal),
567
+ ready: (signal) => this.call("/readyz", "GET", undefined, signal),
568
+ };
569
+ }
570
+ get webhooks() {
571
+ return {
572
+ verify: async (request, expectedToken) => {
573
+ if (!expectedToken)
574
+ throw new Error("Constellation webhook token is not configured");
575
+ const actual = request.headers.get("x-constellation-webhook-token") ?? "";
576
+ let mismatch = actual.length ^ expectedToken.length;
577
+ for (let i = 0; i < Math.max(actual.length, expectedToken.length); i++)
578
+ mismatch |= (actual.charCodeAt(i) || 0) ^ (expectedToken.charCodeAt(i) || 0);
579
+ if (mismatch !== 0)
580
+ throw new Error("Invalid Constellation webhook token");
581
+ const body = await request.json();
582
+ if (!body || typeof body !== "object")
583
+ throw new Error("Invalid Constellation webhook body");
584
+ const event = body;
585
+ if (typeof event.requestId !== "string" || !event.requestId)
586
+ throw new Error("Invalid Constellation request ID");
587
+ if (typeof event.inputId === "string" && ["tool", "prepare_step", "caller_review", "user_input"].includes(String(event.kind))) {
588
+ if (request.headers.get("x-constellation-input-id") !== event.inputId || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload))
589
+ throw new Error("Invalid Constellation input notification");
590
+ return event;
591
+ }
592
+ if (event.kind === "request.model_events" && Number.isSafeInteger(event.afterSeq) && Array.isArray(event.events))
593
+ return event;
594
+ if (["request.completed", "request.failed", "request.cancelled"].includes(String(event.kind)) && Number.isSafeInteger(event.eventSeq))
595
+ return event;
596
+ throw new Error("Unknown Constellation webhook event");
597
+ },
598
+ };
599
+ }
410
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "TypeScript client for durable model requests and caller-controlled agent execution",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",