@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 +47 -121
- package/dist/index.d.ts +305 -46
- package/dist/index.js +207 -54
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,155 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lionhummer/constellation
|
|
2
2
|
|
|
3
|
-
Constellation
|
|
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
|
|
18
|
-
apiKey: process.env.
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
messages: [{ role: "user", text: "
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
34
|
+
## Events and caller work
|
|
49
35
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
56
|
+
await client.inputs.handle(id, inputId, handlers)
|
|
119
57
|
```
|
|
120
58
|
|
|
121
|
-
|
|
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
|
-
##
|
|
61
|
+
## Webhook
|
|
126
62
|
|
|
127
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
## Inspect and recover
|
|
77
|
+
## Inspection and other APIs
|
|
142
78
|
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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" | "
|
|
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" | "
|
|
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
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
}) =>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
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
|
-
|
|
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
|
-
/**
|
|
265
|
-
async
|
|
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
|
-
|
|
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
|
-
!(
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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(
|
|
331
|
-
await
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "TypeScript client for durable model requests and caller
|
|
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": {
|