@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 +123 -106
- package/dist/index.d.ts +311 -28
- package/dist/index.js +196 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,155 +1,172 @@
|
|
|
1
1
|
# Constellation TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
apiKey: process.env.
|
|
13
|
+
baseUrl: "https://your-constellation-host.example",
|
|
14
|
+
apiKey: process.env.CONSTELLATION_EXECUTION_KEY!,
|
|
19
15
|
});
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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.
|
|
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
|
-
|
|
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.
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
### Tool-capable run
|
|
49
51
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
prepareStep: async (state, { signal, parentSpanId }) => ({
|
|
105
|
+
stepInput: await buildCompleteNextStep(state, { signal, parentSpanId }),
|
|
106
|
+
}),
|
|
107
|
+
} satisfies InputHandlers;
|
|
119
108
|
```
|
|
120
109
|
|
|
121
|
-
|
|
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
|
-
|
|
112
|
+
## Webhooks and events
|
|
124
113
|
|
|
125
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
154
|
+
## Other namespaces
|
|
142
155
|
|
|
143
|
-
|
|
|
156
|
+
| Namespace | Methods |
|
|
144
157
|
| --- | --- |
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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,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
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
}
|