@lionhummer/constellation 0.1.1 → 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 +144 -97
- package/dist/index.d.ts +352 -23
- package/dist/index.js +340 -54
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,125 +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
|
-
|
|
8
|
-
this monorepo is under development.
|
|
9
|
-
External SDK calls use `/v1/*` with a bearer key. The first-party console sets
|
|
10
|
-
`apiPrefix: "/internal"` and supplies its session-aware fetch adapter; that
|
|
11
|
-
prefix is for the signed-in console, not third-party integrations.
|
|
5
|
+
```sh
|
|
6
|
+
npm install @lionhummer/constellation
|
|
7
|
+
```
|
|
12
8
|
|
|
13
9
|
```ts
|
|
14
|
-
import { Constellation, uuid7 } from "@lionhummer/constellation";
|
|
15
|
-
|
|
16
|
-
const client = new Constellation({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
externalId: threadId, // Search/group identifier; no shared history.
|
|
20
|
-
workspaceId,
|
|
21
|
-
model: "obvious/fusion-1p0", // Physical model or virtual SKU.
|
|
22
|
-
kind: "run",
|
|
23
|
-
messages: [{ role: "user", text: "Echo hello using the tool." }],
|
|
24
|
-
tools: [{
|
|
25
|
-
name: "echo",
|
|
26
|
-
inputSchema: {
|
|
27
|
-
type: "object",
|
|
28
|
-
properties: { text: { type: "string" } },
|
|
29
|
-
required: ["text"],
|
|
30
|
-
additionalProperties: false,
|
|
31
|
-
},
|
|
32
|
-
}],
|
|
10
|
+
import { Constellation, uuid7, type InputHandlers } from "@lionhummer/constellation";
|
|
11
|
+
|
|
12
|
+
const client = new Constellation({
|
|
13
|
+
baseUrl: "https://your-constellation-host.example",
|
|
14
|
+
apiKey: process.env.CONSTELLATION_EXECUTION_KEY!,
|
|
33
15
|
});
|
|
16
|
+
```
|
|
17
|
+
|
|
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
|
|
21
|
+
|
|
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. |
|
|
27
|
+
|
|
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
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const { id } = await client.requests.create({
|
|
34
|
+
kind: "generate",
|
|
35
|
+
idempotencyKey: uuid7(),
|
|
36
|
+
workspaceId: "workspace-123",
|
|
37
|
+
model: "catalog/example-model",
|
|
38
|
+
messages: [{ role: "user", text: "Explain this result." }],
|
|
39
|
+
});
|
|
40
|
+
|
|
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;
|
|
44
|
+
}
|
|
45
|
+
const final = await client.requests.get(id);
|
|
46
|
+
```
|
|
47
|
+
|
|
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.
|
|
49
|
+
|
|
50
|
+
### Tool-capable run
|
|
51
|
+
|
|
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:
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
```ts
|
|
55
|
+
const handlers = {
|
|
56
|
+
consumerId: "stable-worker-id",
|
|
37
57
|
tools: {
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
signal
|
|
41
|
-
return args;
|
|
58
|
+
lookup: async (args, { inputId, signal }) => {
|
|
59
|
+
// Deduplicate external effects by inputId: callbacks can be delivered again.
|
|
60
|
+
return lookupOnce(args, inputId, signal);
|
|
42
61
|
},
|
|
43
62
|
},
|
|
44
|
-
}
|
|
45
|
-
|
|
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);
|
|
46
70
|
```
|
|
47
71
|
|
|
48
|
-
|
|
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.
|
|
73
|
+
|
|
74
|
+
### Caller-controlled agent
|
|
49
75
|
|
|
50
|
-
|
|
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.
|
|
51
77
|
|
|
52
78
|
```ts
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
const { id } = await client.requests.create({
|
|
80
|
+
kind: "agent",
|
|
81
|
+
idempotencyKey: uuid7(),
|
|
82
|
+
workspaceId: "workspace-123",
|
|
83
|
+
externalId: "conversation-456",
|
|
84
|
+
model: "catalog/example-model",
|
|
85
|
+
instructions: "Work carefully and use tools when needed.",
|
|
86
|
+
messages: fullFirstStepHistory,
|
|
87
|
+
tools: toolDefinitions,
|
|
88
|
+
parameters: {},
|
|
89
|
+
maxOutputTokens: 4096,
|
|
56
90
|
});
|
|
57
|
-
await span.end({ endedAt: lookupEndedAt, status: "ok" });
|
|
58
|
-
await client.addLogs(request.id, [{
|
|
59
|
-
id: uuid7(), spanId: span.id, level: "info", message: "Lookup completed",
|
|
60
|
-
timestamp: lookupEndedAt, attributes: {},
|
|
61
|
-
}]);
|
|
62
91
|
```
|
|
63
92
|
|
|
64
|
-
|
|
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.
|
|
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
|
+
}),
|
|
101
|
+
tools: {
|
|
102
|
+
lookup: async (args, { inputId, signal }) => lookupOnce(args, inputId, signal),
|
|
103
|
+
},
|
|
104
|
+
prepareStep: async (state, { signal, parentSpanId }) => ({
|
|
105
|
+
stepInput: await buildCompleteNextStep(state, { signal, parentSpanId }),
|
|
106
|
+
}),
|
|
107
|
+
} satisfies InputHandlers;
|
|
108
|
+
```
|
|
109
|
+
|
|
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.
|
|
111
|
+
|
|
112
|
+
## Webhooks and events
|
|
113
|
+
|
|
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:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
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.
|
|
65
130
|
|
|
66
|
-
|
|
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.
|
|
67
132
|
|
|
68
|
-
|
|
133
|
+
## Inspect and instrument
|
|
69
134
|
|
|
70
135
|
```ts
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const
|
|
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
|
+
}
|
|
74
148
|
```
|
|
75
149
|
|
|
76
|
-
`
|
|
77
|
-
at `/test` is the SDK playground, with real LLM calls and local mock tools. Its
|
|
78
|
-
Resolve only option calls this same method. Inspection includes original `input`
|
|
79
|
-
and the current `effectiveInput` after application context refreshes.
|
|
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.
|
|
80
151
|
|
|
81
|
-
|
|
82
|
-
`records(id, "logs", { level: "error", q: "tool", offset: 0, limit: 25 })`.
|
|
83
|
-
Supported record kinds are `steps`, `attempts`, `inputs`, `spans`, and `logs`;
|
|
84
|
-
responses contain `items`, `total`, `offset`, and `limit`. Summary inspection
|
|
85
|
-
intentionally contains empty telemetry arrays; `inspect` retains its full behavior.
|
|
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.
|
|
86
153
|
|
|
87
|
-
##
|
|
154
|
+
## Other namespaces
|
|
88
155
|
|
|
89
|
-
|
|
|
156
|
+
| Namespace | Methods |
|
|
90
157
|
| --- | --- |
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`tools`, `parameters`, `maxSteps`, `timeoutMs`, `maxOutputTokens`,
|
|
107
|
-
`prepareStep` and `cache`. Set `cache.mode` to `automatic` for provider-native
|
|
108
|
-
prefix caching or `explicit` with up to four `points` for exact breakpoints.
|
|
109
|
-
Points target tools, instructions, or a message index and optional `partIndex`
|
|
110
|
-
within `contentParts`. Optional `affinityKey` and `isolationKey` are sent only
|
|
111
|
-
through fields configured on the selected provider. An isolation key excludes
|
|
112
|
-
providers without a mapping; explicit mode excludes providers without exact
|
|
113
|
-
marker support. The selected attempt records whether affinity was applied.
|
|
114
|
-
`kind: "generate"` is a single generation;
|
|
115
|
-
`kind: "run"` can pause for tools or context refreshes. The SDK exports
|
|
116
|
-
`uuid7`, typed request/inspection records, and `ConstellationError` with HTTP status,
|
|
117
|
-
error code, and message. All calls use the configured timeout (30 seconds by
|
|
118
|
-
default) and an optional abort signal.
|
|
119
|
-
|
|
120
|
-
This package covers execution requests. Model discovery (`GET /v1/models`,
|
|
121
|
-
`POST /v1/registry/resolve`), provider HTTP capture, the management APIs, and
|
|
122
|
-
operator APIs are available over HTTP; see the [HTTP API guide](../../docs/registry-api.md)
|
|
123
|
-
and the in-app Documentation page for those endpoint contracts. `events()` uses
|
|
124
|
-
SSE and reconnects from the durable cursor. Aborting `drive()` disconnects the consumer but does not
|
|
125
|
-
cancel the Constellation request.
|
|
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";
|
|
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,25 @@ 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;
|
|
60
74
|
externalId: string | null;
|
|
61
75
|
workspaceId: string;
|
|
62
76
|
userId: string | null;
|
|
63
|
-
kind: "generate" | "run";
|
|
77
|
+
kind: "generate" | "run" | "agent";
|
|
64
78
|
requestedModel: string;
|
|
65
79
|
virtualModel: boolean;
|
|
66
80
|
planId: string | null;
|
|
@@ -88,6 +102,9 @@ export type Step = {
|
|
|
88
102
|
idx: number;
|
|
89
103
|
input: Json;
|
|
90
104
|
inputHash: string;
|
|
105
|
+
catalogRevision: number | null;
|
|
106
|
+
resolution: Json;
|
|
107
|
+
rules: Json;
|
|
91
108
|
status: string;
|
|
92
109
|
output: Json;
|
|
93
110
|
usage: Json;
|
|
@@ -124,21 +141,54 @@ export type Attempt = {
|
|
|
124
141
|
httpStatus: number | null;
|
|
125
142
|
ttfbMs: number | null;
|
|
126
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;
|
|
127
148
|
startedAt: string;
|
|
128
149
|
endedAt: string | null;
|
|
129
150
|
};
|
|
151
|
+
export type PreparedStep = {
|
|
152
|
+
model: string;
|
|
153
|
+
instructions: string;
|
|
154
|
+
messages: Message[];
|
|
155
|
+
tools: Tool[];
|
|
156
|
+
parameters: Record<string, Json>;
|
|
157
|
+
maxOutputTokens: number;
|
|
158
|
+
cache: RequestInput["cache"] | null;
|
|
159
|
+
};
|
|
160
|
+
export type CallerReview = {
|
|
161
|
+
stage: "after_model" | "after_tools";
|
|
162
|
+
stepId: string;
|
|
163
|
+
toolCalls?: ToolCall[];
|
|
164
|
+
toolResults?: Message["toolResults"];
|
|
165
|
+
usage?: Json;
|
|
166
|
+
customerCost?: string | null;
|
|
167
|
+
supplierCost?: string | null;
|
|
168
|
+
customerPrice?: Json;
|
|
169
|
+
output?: Json;
|
|
170
|
+
};
|
|
130
171
|
export type PendingInput = {
|
|
131
172
|
id: string;
|
|
132
173
|
requestId: string;
|
|
133
174
|
stepId: string | null;
|
|
134
|
-
kind: "tool" | "prepare_step" | "user_input";
|
|
175
|
+
kind: "tool" | "prepare_step" | "caller_review" | "user_input";
|
|
135
176
|
payload: {
|
|
136
177
|
providerCallId?: string;
|
|
137
178
|
name?: string;
|
|
138
179
|
arguments?: Json;
|
|
139
180
|
validationError?: string | null;
|
|
181
|
+
stage?: CallerReview["stage"];
|
|
182
|
+
toolCalls?: ToolCall[];
|
|
183
|
+
toolResults?: Message["toolResults"];
|
|
184
|
+
usage?: Json;
|
|
185
|
+
customerCost?: string | null;
|
|
186
|
+
supplierCost?: string | null;
|
|
187
|
+
customerPrice?: Json;
|
|
188
|
+
output?: Json;
|
|
140
189
|
};
|
|
141
190
|
claimId: string | null;
|
|
191
|
+
claimUntil?: string | null;
|
|
142
192
|
response: Json;
|
|
143
193
|
resolvedAt: string | null;
|
|
144
194
|
};
|
|
@@ -146,6 +196,7 @@ export type Span = {
|
|
|
146
196
|
id: string;
|
|
147
197
|
stepId?: string | null;
|
|
148
198
|
attemptId?: string | null;
|
|
199
|
+
inputId?: string | null;
|
|
149
200
|
parentSpanId?: string | null;
|
|
150
201
|
name: string;
|
|
151
202
|
startedAt: string;
|
|
@@ -153,6 +204,39 @@ export type Span = {
|
|
|
153
204
|
status: "running" | "ok" | "error" | "cancelled";
|
|
154
205
|
attributes?: Record<string, Json>;
|
|
155
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
|
+
};
|
|
156
240
|
export type Log = {
|
|
157
241
|
id: string;
|
|
158
242
|
stepId?: string | null;
|
|
@@ -186,14 +270,79 @@ export type InputResponse = {
|
|
|
186
270
|
isError?: boolean;
|
|
187
271
|
instructions?: string;
|
|
188
272
|
tools?: Tool[];
|
|
189
|
-
|
|
273
|
+
stepInput?: PreparedStep;
|
|
274
|
+
action?: "continue" | "refresh_context" | "wait_for_user" | "complete" | "fail";
|
|
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;
|
|
190
299
|
};
|
|
300
|
+
/** @deprecated Use InputHandlers. */
|
|
301
|
+
export type DriveOptions = InputHandlers;
|
|
191
302
|
export type RequestEvent = {
|
|
192
303
|
seq: number;
|
|
193
304
|
type: string;
|
|
194
305
|
data: Record<string, Json>;
|
|
195
306
|
timestamp: string;
|
|
196
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
|
+
};
|
|
197
346
|
export declare class ConstellationError extends Error {
|
|
198
347
|
status: number;
|
|
199
348
|
code: string;
|
|
@@ -216,6 +365,11 @@ export declare class Constellation {
|
|
|
216
365
|
id: string;
|
|
217
366
|
}>;
|
|
218
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>;
|
|
219
373
|
/** Request context without loading historical telemetry. Fetch records by page. */
|
|
220
374
|
inspectSummary(id: string, signal?: AbortSignal): Promise<Inspection>;
|
|
221
375
|
records<K extends keyof RecordTypes>(id: string, kind: K, query?: {
|
|
@@ -265,12 +419,16 @@ export declare class Constellation {
|
|
|
265
419
|
}>;
|
|
266
420
|
cancel(id: string, signal?: AbortSignal): Promise<void>;
|
|
267
421
|
claim(id: string, inputId: string, claimId: string, signal?: AbortSignal): Promise<void>;
|
|
422
|
+
/** Agent claims are leases. Keep one alive while a caller handler is running. */
|
|
423
|
+
private maintainClaim;
|
|
424
|
+
private whileClaimed;
|
|
268
425
|
respond(id: string, inputId: string, response: InputResponse, signal?: AbortSignal): Promise<void>;
|
|
269
426
|
addSpans(id: string, spans: Span[], signal?: AbortSignal): Promise<void>;
|
|
270
427
|
addLogs(id: string, logs: Log[], signal?: AbortSignal): Promise<void>;
|
|
271
428
|
startSpan(id: string, name: string, options?: {
|
|
272
429
|
stepId?: string;
|
|
273
430
|
attemptId?: string;
|
|
431
|
+
inputId?: string;
|
|
274
432
|
parentSpanId?: string;
|
|
275
433
|
startedAt?: string;
|
|
276
434
|
attributes?: Record<string, Json>;
|
|
@@ -287,20 +445,191 @@ export declare class Constellation {
|
|
|
287
445
|
after?: number;
|
|
288
446
|
signal?: AbortSignal;
|
|
289
447
|
}): AsyncGenerator<RequestEvent>;
|
|
290
|
-
/**
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
signal?: AbortSignal;
|
|
296
|
-
}) => Promise<Json>>;
|
|
297
|
-
prepareStep?: (state: Inspection, context: {
|
|
298
|
-
inputId: string;
|
|
299
|
-
signal: AbortSignal;
|
|
300
|
-
}) => Promise<Pick<InputResponse, "instructions" | "tools" | "action">>;
|
|
301
|
-
signal?: AbortSignal;
|
|
302
|
-
consumerId?: string;
|
|
303
|
-
toolTimeoutMs?: number;
|
|
304
|
-
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;
|
|
305
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
|
+
};
|
|
306
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();
|
|
@@ -113,6 +131,48 @@ export class Constellation {
|
|
|
113
131
|
claim(id, inputId, claimId, signal) {
|
|
114
132
|
return this.call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}/claim`, "POST", { claimId }, signal);
|
|
115
133
|
}
|
|
134
|
+
/** Agent claims are leases. Keep one alive while a caller handler is running. */
|
|
135
|
+
maintainClaim(id, inputId, claimId, externalSignal) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const signal = externalSignal
|
|
138
|
+
? AbortSignal.any([externalSignal, controller.signal])
|
|
139
|
+
: controller.signal;
|
|
140
|
+
const renewal = (async () => {
|
|
141
|
+
while (!signal.aborted) {
|
|
142
|
+
try {
|
|
143
|
+
await pause(15_000, signal);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (signal.aborted)
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
await this.claim(id, inputId, claimId, signal);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
controller.abort(error);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
return {
|
|
160
|
+
signal,
|
|
161
|
+
stop: async () => {
|
|
162
|
+
controller.abort();
|
|
163
|
+
await renewal;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async whileClaimed(id, inputId, claimId, leased, operation, signal) {
|
|
168
|
+
const lease = leased ? this.maintainClaim(id, inputId, claimId, signal) : null;
|
|
169
|
+
try {
|
|
170
|
+
return await operation(lease?.signal ?? signal);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await lease?.stop();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
116
176
|
respond(id, inputId, response, signal) {
|
|
117
177
|
return this.call(`/v1/requests/${encodeURIComponent(id)}/inputs/${encodeURIComponent(inputId)}`, "POST", response, signal);
|
|
118
178
|
}
|
|
@@ -219,7 +279,11 @@ export class Constellation {
|
|
|
219
279
|
await pause(500, options.signal);
|
|
220
280
|
}
|
|
221
281
|
}
|
|
222
|
-
/**
|
|
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. */
|
|
223
287
|
async drive(id, options = {}) {
|
|
224
288
|
const consumerId = options.consumerId ?? uuid7();
|
|
225
289
|
while (true) {
|
|
@@ -227,10 +291,20 @@ export class Constellation {
|
|
|
227
291
|
options.onUpdate?.(state);
|
|
228
292
|
if (terminal(state.request.status))
|
|
229
293
|
return state;
|
|
230
|
-
|
|
231
|
-
|
|
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))) {
|
|
303
|
+
if (pending.claimId && pending.claimId !== consumerId &&
|
|
304
|
+
!(state.request.kind === "agent" && pending.claimUntil && Date.parse(pending.claimUntil) <= Date.now()))
|
|
232
305
|
continue;
|
|
233
|
-
|
|
306
|
+
const prepareStep = options.prepareStep;
|
|
307
|
+
if (pending.kind === "prepare_step" && prepareStep) {
|
|
234
308
|
try {
|
|
235
309
|
await this.claim(id, pending.id, consumerId, options.signal);
|
|
236
310
|
}
|
|
@@ -239,27 +313,86 @@ export class Constellation {
|
|
|
239
313
|
continue;
|
|
240
314
|
throw error;
|
|
241
315
|
}
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
317
|
+
let closed = false;
|
|
318
|
+
const preparation = await this.startSpan(id, "caller.prepare_step", { inputId: pending.id }, activeSignal);
|
|
319
|
+
try {
|
|
320
|
+
const signal = activeSignal
|
|
321
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
322
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
323
|
+
const response = await bounded(prepareStep(state, { inputId: pending.id, parentSpanId: preparation.id, signal }), signal);
|
|
324
|
+
await preparation.end({}, activeSignal);
|
|
325
|
+
closed = true;
|
|
326
|
+
if (state.request.kind === "agent")
|
|
327
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
328
|
+
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!activeSignal?.aborted && !closed)
|
|
332
|
+
await preparation.end({ status: "error" }, activeSignal).catch(() => { });
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}, options.signal);
|
|
336
|
+
handled = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const callerReview = options.callerReview;
|
|
340
|
+
if (pending.kind === "caller_review" && callerReview) {
|
|
341
|
+
const stage = pending.payload.stage;
|
|
342
|
+
if (!pending.stepId || (stage !== "after_model" && stage !== "after_tools"))
|
|
343
|
+
throw new Error("Caller review is missing its step or stage");
|
|
344
|
+
const stepId = pending.stepId;
|
|
345
|
+
const review = { ...pending.payload, stepId, stage };
|
|
244
346
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
347
|
+
await this.claim(id, pending.id, consumerId, options.signal);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
351
|
+
continue;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
await this.whileClaimed(id, pending.id, consumerId, true, async (activeSignal) => {
|
|
355
|
+
const reviewSpan = await this.startSpan(id, "caller.review", { stepId, inputId: pending.id, attributes: { stage } }, activeSignal);
|
|
356
|
+
let closed = false;
|
|
357
|
+
try {
|
|
358
|
+
const signal = activeSignal
|
|
359
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
360
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
361
|
+
const response = await bounded(callerReview(review, state, { inputId: pending.id, signal }), signal);
|
|
362
|
+
await reviewSpan.end({}, activeSignal);
|
|
363
|
+
closed = true;
|
|
364
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
365
|
+
await this.respond(id, pending.id, { ...response, claimId: consumerId }, activeSignal);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (!activeSignal?.aborted && !closed)
|
|
369
|
+
await reviewSpan.end({ status: "error" }, activeSignal).catch(() => { });
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}, options.signal);
|
|
373
|
+
handled = true;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const userInput = options.userInput;
|
|
377
|
+
if (pending.kind === "user_input" && userInput) {
|
|
378
|
+
try {
|
|
379
|
+
await this.claim(id, pending.id, consumerId, options.signal);
|
|
255
380
|
}
|
|
256
381
|
catch (error) {
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
.end({ status: "error" }, options.signal)
|
|
260
|
-
.catch(() => { });
|
|
382
|
+
if (error instanceof ConstellationError && error.status === 409)
|
|
383
|
+
continue;
|
|
261
384
|
throw error;
|
|
262
385
|
}
|
|
386
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
387
|
+
const signal = activeSignal
|
|
388
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
389
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
390
|
+
const result = await bounded(userInput(state, { inputId: pending.id, signal }), signal);
|
|
391
|
+
if (state.request.kind === "agent")
|
|
392
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
393
|
+
await this.respond(id, pending.id, { result, claimId: consumerId }, activeSignal);
|
|
394
|
+
}, options.signal);
|
|
395
|
+
handled = true;
|
|
263
396
|
continue;
|
|
264
397
|
}
|
|
265
398
|
if (pending.kind !== "tool")
|
|
@@ -275,40 +408,193 @@ export class Constellation {
|
|
|
275
408
|
continue;
|
|
276
409
|
throw error;
|
|
277
410
|
}
|
|
278
|
-
|
|
279
|
-
stepId: pending.stepId ?? undefined,
|
|
280
|
-
|
|
411
|
+
await this.whileClaimed(id, pending.id, consumerId, state.request.kind === "agent", async (activeSignal) => {
|
|
412
|
+
const span = await this.startSpan(id, `tool.${pending.payload.name}`, { stepId: pending.stepId ?? undefined, inputId: pending.id }, activeSignal);
|
|
413
|
+
let result;
|
|
414
|
+
let isError = false;
|
|
415
|
+
try {
|
|
416
|
+
if (pending.payload.validationError)
|
|
417
|
+
throw new Error(pending.payload.validationError);
|
|
418
|
+
if (!tool)
|
|
419
|
+
throw new Error("Tool unavailable");
|
|
420
|
+
const toolSignal = activeSignal
|
|
421
|
+
? AbortSignal.any([activeSignal, AbortSignal.timeout(options.toolTimeoutMs ?? 30000)])
|
|
422
|
+
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
423
|
+
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
424
|
+
inputId: pending.id,
|
|
425
|
+
requestId: id,
|
|
426
|
+
signal: toolSignal,
|
|
427
|
+
}), toolSignal);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
activeSignal?.throwIfAborted();
|
|
431
|
+
isError = true;
|
|
432
|
+
result = { error: error instanceof Error ? error.message : "Tool failed" };
|
|
433
|
+
}
|
|
434
|
+
await span.end({ status: isError ? "error" : "ok" }, activeSignal);
|
|
435
|
+
if (state.request.kind === "agent")
|
|
436
|
+
await this.claim(id, pending.id, consumerId, activeSignal);
|
|
437
|
+
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, activeSignal);
|
|
281
438
|
}, options.signal);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
throw new Error("Tool unavailable");
|
|
289
|
-
const toolSignal = options.signal
|
|
290
|
-
? AbortSignal.any([
|
|
291
|
-
options.signal,
|
|
292
|
-
AbortSignal.timeout(options.toolTimeoutMs ?? 30000),
|
|
293
|
-
])
|
|
294
|
-
: AbortSignal.timeout(options.toolTimeoutMs ?? 30000);
|
|
295
|
-
result = await bounded(tool(pending.payload.arguments ?? {}, {
|
|
296
|
-
inputId: pending.id,
|
|
297
|
-
requestId: id,
|
|
298
|
-
signal: toolSignal,
|
|
299
|
-
}), toolSignal);
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
options.signal?.throwIfAborted();
|
|
303
|
-
isError = true;
|
|
304
|
-
result = {
|
|
305
|
-
error: error instanceof Error ? error.message : "Tool failed",
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
await span.end({ status: isError ? "error" : "ok" }, options.signal);
|
|
309
|
-
await this.respond(id, pending.id, { result, isError, claimId: consumerId }, 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);
|
|
310
445
|
}
|
|
311
446
|
await pause(500, options.signal);
|
|
312
447
|
}
|
|
313
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
|
+
}
|
|
314
600
|
}
|
package/package.json
CHANGED