@lionhummer/constellation 0.9.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -152
- package/dist/index.js +167 -70
- package/dist/types/client-core/src/agent.d.ts +68 -28
- package/dist/types/client-core/src/index.d.ts +27 -2
- package/dist/types/sdk/src/index.d.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,12 +70,11 @@ console.log(request.status, request.output?.text);
|
|
|
70
70
|
|
|
71
71
|
## Build an agent with tools
|
|
72
72
|
|
|
73
|
-
A tool-using agent needs
|
|
73
|
+
A tool-using agent needs three pieces:
|
|
74
74
|
|
|
75
|
-
1. **A webhook** that receives events
|
|
75
|
+
1. **A webhook** that receives events.
|
|
76
76
|
2. **Hooks** that run your tools and build the next input.
|
|
77
|
-
3. **An `
|
|
78
|
-
4. **`processEvent`**, which ties the other three together.
|
|
77
|
+
3. **An `EventReceiver`**, which takes each event, runs your hooks in the background and sends the response. You don't store events or responses: Constellation re-sends an event that stays unanswered, and deduplicates your answers.
|
|
79
78
|
|
|
80
79
|
### 1. Turn on events
|
|
81
80
|
|
|
@@ -142,112 +141,19 @@ const hooks: AgentHooks = {
|
|
|
142
141
|
|
|
143
142
|
`conversation.settings` must repeat the first request's `workspaceId`, `userId`, `externalId`, `model`, `planId`, `events` and `timeoutMs`. A continuation that changes any of them is rejected. You may change `messages`, `instructions`, `tools`, `parameters` and `cache`.
|
|
144
143
|
|
|
145
|
-
### 3.
|
|
146
|
-
|
|
147
|
-
The store is a small table in your own database. It guarantees two things:
|
|
148
|
-
|
|
149
|
-
- an event you acknowledged is never lost;
|
|
150
|
-
- a response you prepared is sent unchanged, even after a crash.
|
|
151
|
-
|
|
152
|
-
Do not use an in-memory map in production. Here is a complete PostgreSQL version using [`pg`](https://node-postgres.com/):
|
|
153
|
-
|
|
154
|
-
```sql
|
|
155
|
-
CREATE TABLE constellation_events (
|
|
156
|
-
event_id text PRIMARY KEY,
|
|
157
|
-
payload jsonb NOT NULL,
|
|
158
|
-
prepared boolean NOT NULL DEFAULT false,
|
|
159
|
-
response jsonb, -- the exact body we send (or resend)
|
|
160
|
-
state text NOT NULL DEFAULT 'pending', -- pending | done | rejected
|
|
161
|
-
rejection jsonb,
|
|
162
|
-
failures integer NOT NULL DEFAULT 0,
|
|
163
|
-
last_error jsonb,
|
|
164
|
-
first_failure_at timestamptz,
|
|
165
|
-
last_failure_at timestamptz,
|
|
166
|
-
received_at timestamptz NOT NULL DEFAULT now()
|
|
167
|
-
);
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
import pg from "pg";
|
|
172
|
-
import { PermanentEventError, type EventStore, type WebhookEvent } from "@lionhummer/constellation";
|
|
173
|
-
|
|
174
|
-
const db = new pg.Pool();
|
|
175
|
-
|
|
176
|
-
export const store: EventStore & { pending(): Promise<WebhookEvent[]> } = {
|
|
177
|
-
// Save the event before acknowledging the webhook. Duplicates must match.
|
|
178
|
-
async accept(event) {
|
|
179
|
-
await db.query(
|
|
180
|
-
`INSERT INTO constellation_events (event_id, payload) VALUES ($1, $2)
|
|
181
|
-
ON CONFLICT (event_id) DO NOTHING`, [event.id, event]);
|
|
182
|
-
const { rows } = await db.query(
|
|
183
|
-
`SELECT payload = $2::jsonb AS same FROM constellation_events WHERE event_id = $1`, [event.id, event]);
|
|
184
|
-
if (!rows[0]?.same) throw new Error(`event ${event.id} redelivered with a different payload`);
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
// Run `prepare` at most once per event at a time, and save its result before returning it.
|
|
188
|
-
async prepare(event, prepare) {
|
|
189
|
-
const tx = await db.connect();
|
|
190
|
-
try {
|
|
191
|
-
await tx.query("BEGIN");
|
|
192
|
-
const { rows } = await tx.query(
|
|
193
|
-
`SELECT prepared, response, state, rejection FROM constellation_events
|
|
194
|
-
WHERE event_id = $1 FOR UPDATE`, [event.id]); // serializes concurrent processing
|
|
195
|
-
const row = rows[0];
|
|
196
|
-
if (row.state === "rejected") {
|
|
197
|
-
const r = row.rejection;
|
|
198
|
-
throw new PermanentEventError(r.status, r.code, r.message);
|
|
199
|
-
}
|
|
200
|
-
if (row.prepared) { await tx.query("COMMIT"); return row.response; }
|
|
201
|
-
const response = await prepare(); // runs your tools and hooks
|
|
202
|
-
await tx.query(
|
|
203
|
-
`UPDATE constellation_events SET prepared = true, response = $2 WHERE event_id = $1`,
|
|
204
|
-
[event.id, response]);
|
|
205
|
-
await tx.query("COMMIT");
|
|
206
|
-
return response;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
await tx.query("ROLLBACK");
|
|
209
|
-
throw error;
|
|
210
|
-
} finally {
|
|
211
|
-
tx.release();
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
async complete(eventId) {
|
|
216
|
-
await db.query(`UPDATE constellation_events SET state = 'done' WHERE event_id = $1`, [eventId]);
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
// A permanent rejection: never resend this response.
|
|
220
|
-
async reject(eventId, error) {
|
|
221
|
-
await db.query(
|
|
222
|
-
`UPDATE constellation_events SET state = 'rejected', rejection = $2 WHERE event_id = $1`,
|
|
223
|
-
[eventId, { status: error.status, code: error.code, message: error.message }]);
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
// Optional: a retryable failure. Alert when failures pile up or get old.
|
|
227
|
-
async retry(eventId, error) {
|
|
228
|
-
await db.query(
|
|
229
|
-
`UPDATE constellation_events SET failures = failures + 1, last_error = $2,
|
|
230
|
-
first_failure_at = coalesce(first_failure_at, now()), last_failure_at = now()
|
|
231
|
-
WHERE event_id = $1`, [eventId, error]);
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
// Not part of EventStore: used by the recovery sweep below.
|
|
235
|
-
async pending() {
|
|
236
|
-
const { rows } = await db.query(
|
|
237
|
-
`SELECT payload FROM constellation_events WHERE state = 'pending' ORDER BY received_at LIMIT 100`);
|
|
238
|
-
return rows.map((row) => row.payload);
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
`prepare` keeps a row lock while your tools run. That is acceptable for tools that finish in seconds or minutes. For very long work, replace the lock with a lease column.
|
|
144
|
+
### 3. Receive webhooks
|
|
244
145
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
Acknowledge quickly, then process in the background. The webhook handler below works with any framework that uses the web `Request`/`Response` types (Hono, Next.js route handlers, Bun, Deno):
|
|
146
|
+
Acknowledge quickly, then process in the background. The handler below works with any framework that uses the web `Request`/`Response` types (Hono, Next.js route handlers, Bun, Deno):
|
|
248
147
|
|
|
249
148
|
```ts
|
|
250
|
-
import {
|
|
149
|
+
import { EventReceiver, redisEventLock } from "@lionhummer/constellation";
|
|
150
|
+
|
|
151
|
+
const receiver = new EventReceiver(client, hooks, {
|
|
152
|
+
// One process: leave this out (an in-memory lock is used).
|
|
153
|
+
// Several processes or pods: share the lock so only one works on an event at a time.
|
|
154
|
+
lock: redisEventLock((args) => redis.call(...args)), // ioredis; node-redis: (args) => redis.sendCommand(args)
|
|
155
|
+
onFailure: (eventId, error) => logger.warn({ eventId, error }, error.willRetry ? "retrying event" : "event will be re-sent"),
|
|
156
|
+
});
|
|
251
157
|
|
|
252
158
|
export async function webhook(request: Request): Promise<Response> {
|
|
253
159
|
let event;
|
|
@@ -256,68 +162,63 @@ export async function webhook(request: Request): Promise<Response> {
|
|
|
256
162
|
} catch {
|
|
257
163
|
return new Response(null, { status: 401 });
|
|
258
164
|
}
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
|
|
165
|
+
const run = await receiver.claim(event);
|
|
166
|
+
if (!run) return new Response(null, { status: 202 }); // another process is working on it
|
|
167
|
+
void run().then((outcome) => {
|
|
168
|
+
if (outcome.status === "answered" && outcome.ended) markConversationEnded(event.requestId, outcome.ended);
|
|
169
|
+
}, () => {}); // a failure leaves the event unanswered; Constellation re-sends it
|
|
170
|
+
return new Response(null, { status: 204 });
|
|
262
171
|
}
|
|
263
|
-
|
|
264
|
-
async function handle(event: WebhookEvent) {
|
|
265
|
-
try {
|
|
266
|
-
await processEvent(client, store, event, hooks);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
if (error instanceof PermanentEventError) {
|
|
269
|
-
await markConversationFailed(event.requestId, error); // will not be retried
|
|
270
|
-
} else {
|
|
271
|
-
logger.warn({ eventId: event.id, error }, "event processing will be retried");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Optional: resume unfinished events right after a restart.
|
|
277
|
-
for (const event of await store.pending()) void handle(event);
|
|
278
172
|
```
|
|
279
173
|
|
|
280
174
|
Don't run tools inside the webhook call. Constellation waits 5 seconds for the acknowledgement by default, and nothing in Constellation waits for your tools.
|
|
281
175
|
|
|
282
|
-
|
|
176
|
+
`outcome.ended` tells you when the request is over: `completed` or `failed` after your final answer, `closed` when it had already been cancelled or had expired. Requests that fail or are cancelled on the Constellation side arrive as `model.failed` and `model.cancelled` events. You never need to read the request back.
|
|
283
177
|
|
|
284
|
-
|
|
178
|
+
### Recovery
|
|
285
179
|
|
|
286
|
-
|
|
180
|
+
A failure that may pass, such as a database lock timeout or a network error, is retried in the same process after 1, 3 and 9 seconds while it still holds the event (`retryDelaysMs`; `[]` turns this off). A re-send arriving meanwhile is answered `202`. Your own code can throw `PermanentEventError` to skip these retries.
|
|
287
181
|
|
|
288
|
-
|
|
289
|
-
- For `model.completed`, it runs every tool call in parallel and calls `onStepComplete`. To continue, it calls `prepareRequest`, which produces a continuation. To stop, it produces an `observe` response.
|
|
290
|
-
- For `model.failed` and `model.cancelled`, it calls `onError(event.data)` and produces an `observe` response.
|
|
182
|
+
If your process dies while handling an event, you don't need a poller or a store to recover it. Constellation sends an unanswered `model.completed` event again after the response window (10 minutes by default; operators can lower it, for example to 2 minutes), up to 3 times. A re-sent event has the same `id`:
|
|
291
183
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
3. **Record the outcome**:
|
|
184
|
+
- while another process still holds the event's lock, `claim` returns `null` and you answer `202`. Constellation then checks again after another window without counting a re-send;
|
|
185
|
+
- once the holder is gone, its lock expires (60 s by default, renewed while it works) and the re-send is processed from the start.
|
|
295
186
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
| `200`, `outcome: "already_accepted"` | A retry of a response that was already accepted, for example after a lost reply. Nothing runs twice. | `store.complete`, resolves |
|
|
300
|
-
| `200`, `outcome: "event_closed"` | The request was cancelled or expired before your continuation arrived. Nothing continues; your telemetry is still recorded (`observationsRecorded`). | `store.complete`, resolves |
|
|
301
|
-
| `400 bad_request` | The response itself is invalid, for example a changed model or a bad message. | `store.reject`, throws `PermanentEventError` |
|
|
302
|
-
| `409 response_conflict` | A *different* response to this event was already accepted. | `store.reject`, throws `PermanentEventError` |
|
|
303
|
-
| `410 observations_expired` | The request ended more than 24 hours ago. | `store.reject`, throws `PermanentEventError` |
|
|
304
|
-
| Anything else: network error, timeout, `5xx`, `429`, `409 conflict`, `401`, `403` | Temporary. | `store.retry`, throws. The next attempt resends the same body. |
|
|
187
|
+
If you answer an event twice, for example because the reply to your first answer was lost, Constellation keeps the first answer and reports the second as `already_accepted` with `bodyMatched: false`. Nothing continues twice.
|
|
188
|
+
|
|
189
|
+
## What happens to each event
|
|
305
190
|
|
|
306
|
-
|
|
191
|
+
1. **Take the lock** for the event. If another process holds it: `busy` (answer `202`).
|
|
192
|
+
2. **Prepare the response:**
|
|
193
|
+
- For `model.completed`, run every tool call in parallel and call `onStepComplete`. To continue, call `prepareRequest`, which produces a continuation. To stop, produce an `observe` response.
|
|
194
|
+
- A tool call marked `invalid` (its arguments were not valid JSON) is never run. Its result is an error from `invalidToolInputResult` with `isError: true`, so the model can correct the call on the next step, as the AI SDK does.
|
|
195
|
+
- For `model.failed` and `model.cancelled`, call `onError(event.data)` and produce an `observe` response.
|
|
196
|
+
|
|
197
|
+
The response carries the idempotency key `event.id`, `inReplyTo: { requestId, eventId }` and the telemetry recorded while preparing.
|
|
198
|
+
3. **Submit it** and release the lock:
|
|
199
|
+
|
|
200
|
+
| Server answer | Meaning | Result |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| `200`, `outcome: "accepted"` | Your response was accepted. A continuation starts the next step (`scheduled: true`). | `answered` |
|
|
203
|
+
| `200`, `outcome: "already_accepted"` | This event was already answered by you. `bodyMatched: false` means the earlier answer differed and stands. | `answered` |
|
|
204
|
+
| `200`, `outcome: "event_closed"` | The request was cancelled or expired before your continuation arrived. Your telemetry is still recorded (`observationsRecorded`). | `answered`, `ended: "closed"` |
|
|
205
|
+
| A permanent rejection (`PERMANENT_RESPONSE_CODES`, for example `400 bad_request`) of a continuation | The continuation can never be accepted. | The SDK sends an `observe` response with the error instead, which ends the request as failed: `ended: "failed"` |
|
|
206
|
+
| `401`, `403` | Key or scope problem. | `onFailure` runs and the call throws. The event stays unanswered and is re-sent. |
|
|
207
|
+
| Anything else: network error, timeout, `5xx`, `429`, `409 conflict`, or an error thrown while preparing | Temporary. | `onFailure` runs with `willRetry: true` and the attempt is repeated after the next delay. After the last one, `onFailure` runs with `willRetry: false`, the call throws, and the event is re-sent. |
|
|
307
208
|
|
|
308
|
-
|
|
209
|
+
While processing, the SDK renews the lock every third of its lease. If renewal fails, it aborts its own work through `signal`, so a re-send taken over elsewhere doesn't run alongside it.
|
|
309
210
|
|
|
310
211
|
## When your code fails
|
|
311
212
|
|
|
312
213
|
| What happened | Without `onError` | With `onError` |
|
|
313
214
|
| --- | --- | --- |
|
|
314
|
-
| A tool or hook throws |
|
|
315
|
-
| No handler for a tool the model called, or `prepareRequest` is missing when continuing | The request
|
|
215
|
+
| A tool or hook throws | The event stays unanswered and is re-sent. Tools may run again, so use `effectId`. | `onError` runs. If it returns, the request stops and the error is recorded in telemetry. If it throws, the event is re-sent. |
|
|
216
|
+
| No handler for a tool the model called, or `prepareRequest` is missing when continuing | The request ends as **failed** (`invalid_hook_configuration`). | `onError` runs, as above. |
|
|
316
217
|
| The model step failed or was cancelled (`model.failed`, `model.cancelled`) | An `observe` response records your telemetry. | `onError(event.data)` runs first. |
|
|
317
218
|
|
|
318
219
|
## Side effects and duplicates
|
|
319
220
|
|
|
320
|
-
|
|
221
|
+
Delivery is at least once. A crash after a tool ran but before the answer was accepted means the tool runs again when the event is re-sent. Constellation cannot make your side effects exactly-once; your code must:
|
|
321
222
|
|
|
322
223
|
- pass `effectId` to anything with side effects (payments, emails, writes). It is stable for one tool call of one event;
|
|
323
224
|
- settle per-step billing or usage once per `event.id`;
|
|
@@ -325,7 +226,7 @@ Hooks can run more than once. A crash after a tool ran, but before the prepared
|
|
|
325
226
|
|
|
326
227
|
## Live output
|
|
327
228
|
|
|
328
|
-
`requests.streamEvents(id, { after, signal })` streams `model.delta` events (text and reasoning as it is generated), followed by each step's final event. It reconnects automatically. To resume after a restart, pass the last `event.sequence` you processed as `after`. Streaming is for display only: run tools from the webhook, never from the stream. Stopping the stream does not cancel the request; `requests.cancel(id)` does.
|
|
229
|
+
`requests.streamEvents(id, { after, signal })` streams `model.delta` events (text and reasoning as it is generated), followed by each step's final event. It reconnects automatically and ends when the request has ended: the server closes the stream with an end frame after the request's last event, so the loop finishes without any state polling. To resume after a restart, pass the last `event.sequence` you processed as `after`. Streaming is for display only: run tools from the webhook, never from the stream. Stopping the stream does not cancel the request; `requests.cancel(id)` does.
|
|
329
230
|
|
|
330
231
|
## Telemetry
|
|
331
232
|
|
|
@@ -427,7 +328,7 @@ A failed provider call falls back to the next provider only before any output ha
|
|
|
427
328
|
| `requests.preview(input)` | Validate a complete input and show routing, without calling a provider. |
|
|
428
329
|
| `requests.cancel(id)` | Cancel a request. |
|
|
429
330
|
| `openers.create(input)`, `openers.sentence(id)` | Start an opening line for a just-sent message (null when openers are turned off), then wait for its sentence (null when skipped or failed). |
|
|
430
|
-
| `requests.getState(id)` | Committed state: status, steps, attempt outcomes, usage and cost. |
|
|
331
|
+
| `requests.getState(id)` | Committed state: status, steps, attempt outcomes, usage and cost. For inspection and support tools; event processing never needs it, because boundary events carry the step number, step input and request identity. |
|
|
431
332
|
| `requests.getSummary(id)`, `requests.get(id)` | Request detail, without or with spans and logs. |
|
|
432
333
|
| `requests.list(query)` | Page through requests, e.g. by `externalId` or `status`. |
|
|
433
334
|
| `requests.listEvents(id, after)`, `requests.streamEvents(id)` | Replay events after a sequence number, or stream them live. |
|
|
@@ -440,7 +341,7 @@ A failed provider call falls back to the next provider only before any output ha
|
|
|
440
341
|
| `management.registry.*` | Edit the model registry. Needs management permission. |
|
|
441
342
|
| `discover()` | The OpenAPI document for exactly what this key may call. |
|
|
442
343
|
|
|
443
|
-
Exports for event processing: `
|
|
344
|
+
Exports for event processing: `EventReceiver`, `processEvent` (inline: lock, prepare, submit), `prepareResponse` (build a response without submitting it), `MemoryEventLock`, `redisEventLock`, `invalidToolInputResult`, `ClientTelemetry`, `PermanentEventError`, `HookConfigurationError`, `PERMANENT_RESPONSE_CODES`, `ConstellationError` and `uuid7`.
|
|
444
345
|
|
|
445
346
|
## Troubleshooting
|
|
446
347
|
|
|
@@ -448,7 +349,7 @@ Exports for event processing: `processEvent`, `prepareResponse` (build a respons
|
|
|
448
349
|
| --- | --- |
|
|
449
350
|
| `models.list` is empty | The key's workspace/user scope, an access rule for that workspace, and an enabled model and offering. |
|
|
450
351
|
| `terminal: "notify"` is rejected | The execution key has no webhook URL configured. |
|
|
451
|
-
| The same webhook keeps arriving | Your endpoint returned non-2xx or took longer than the delivery timeout (5 s by default).
|
|
352
|
+
| The same webhook keeps arriving | Your endpoint returned non-2xx or took longer than the delivery timeout (5 s by default). Answer after `receiver.claim` and process in the background. |
|
|
452
353
|
| `400` on a continuation | Model, workspace, user, `externalId`, plan, events or `timeoutMs` differ from the first request, or the message history is malformed. |
|
|
453
354
|
| `409 response_conflict` | Your code produced a different response for an event that was already answered. Answers must come from the saved response. |
|
|
454
355
|
| Tools ran twice | Expected after a crash. Make side effects idempotent with `effectId`. |
|
package/dist/index.js
CHANGED
|
@@ -253,8 +253,11 @@ class RequestClient {
|
|
|
253
253
|
`)) >= 0) {
|
|
254
254
|
const frame = buffer.slice(0, boundary);
|
|
255
255
|
buffer = buffer.slice(boundary + 2);
|
|
256
|
-
const
|
|
257
|
-
`)
|
|
256
|
+
const lines = frame.split(`
|
|
257
|
+
`);
|
|
258
|
+
if (lines.some((line) => line === "event: end" || line === "event:end"))
|
|
259
|
+
return;
|
|
260
|
+
const data = lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join(`
|
|
258
261
|
`);
|
|
259
262
|
if (!data)
|
|
260
263
|
continue;
|
|
@@ -270,41 +273,9 @@ class RequestClient {
|
|
|
270
273
|
} finally {
|
|
271
274
|
await reader.cancel().catch(() => {});
|
|
272
275
|
}
|
|
273
|
-
const inspection = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/state`, options.signal);
|
|
274
|
-
if (["completed", "failed", "cancelled"].includes(inspection.request.status)) {
|
|
275
|
-
while (true) {
|
|
276
|
-
const before = after;
|
|
277
|
-
const tail = await this.#replayRead(`/v1/requests/${encodeURIComponent(id)}/events?after=${after}`, options.signal);
|
|
278
|
-
for (const event of tail.items) {
|
|
279
|
-
if (event.sequence <= after)
|
|
280
|
-
continue;
|
|
281
|
-
after = event.sequence;
|
|
282
|
-
yield event;
|
|
283
|
-
}
|
|
284
|
-
if (tail.items.length === 0)
|
|
285
|
-
return;
|
|
286
|
-
if (after === before)
|
|
287
|
-
throw new Error("Replay page did not advance the event cursor");
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
276
|
await pause(500, options.signal);
|
|
291
277
|
}
|
|
292
278
|
}
|
|
293
|
-
async#replayRead(path, signal) {
|
|
294
|
-
for (;; ) {
|
|
295
|
-
signal?.throwIfAborted();
|
|
296
|
-
try {
|
|
297
|
-
return await this.#call(path, "GET", undefined, signal);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
signal?.throwIfAborted();
|
|
300
|
-
if (error instanceof ConstellationError && error.status !== 429 && error.status < 500)
|
|
301
|
-
throw error;
|
|
302
|
-
if (error instanceof SyntaxError)
|
|
303
|
-
throw error;
|
|
304
|
-
await pause(500, signal);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
279
|
get models() {
|
|
309
280
|
return {
|
|
310
281
|
list: (scope, signal) => this.#call(`/v1/models${queryString(scope)}`, "GET", undefined, signal),
|
|
@@ -424,15 +395,82 @@ class HookConfigurationError extends PermanentEventError {
|
|
|
424
395
|
}
|
|
425
396
|
}
|
|
426
397
|
|
|
427
|
-
class
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
398
|
+
class MemoryEventLock {
|
|
399
|
+
#held = new Map;
|
|
400
|
+
async acquire(eventId, ttlMs) {
|
|
401
|
+
const until = this.#held.get(eventId);
|
|
402
|
+
if (until !== undefined && until > Date.now())
|
|
403
|
+
return false;
|
|
404
|
+
this.#held.set(eventId, Date.now() + ttlMs);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
async renew(eventId, ttlMs) {
|
|
408
|
+
if (!this.#held.has(eventId))
|
|
409
|
+
return false;
|
|
410
|
+
this.#held.set(eventId, Date.now() + ttlMs);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
async release(eventId) {
|
|
414
|
+
this.#held.delete(eventId);
|
|
431
415
|
}
|
|
432
|
-
|
|
433
|
-
|
|
416
|
+
}
|
|
417
|
+
var RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
|
|
418
|
+
var RELEASE_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
|
419
|
+
function redisEventLock(command, options = {}) {
|
|
420
|
+
const prefix = options.prefix ?? "constellation:event-lock:";
|
|
421
|
+
const tokens = new Map;
|
|
422
|
+
return {
|
|
423
|
+
async acquire(eventId, ttlMs) {
|
|
424
|
+
const token = uuid7();
|
|
425
|
+
const reply = await command(["SET", prefix + eventId, token, "NX", "PX", String(ttlMs)]);
|
|
426
|
+
if (reply !== "OK")
|
|
427
|
+
return false;
|
|
428
|
+
tokens.set(eventId, token);
|
|
429
|
+
return true;
|
|
430
|
+
},
|
|
431
|
+
async renew(eventId, ttlMs) {
|
|
432
|
+
const token = tokens.get(eventId);
|
|
433
|
+
if (!token)
|
|
434
|
+
return false;
|
|
435
|
+
return Number(await command(["EVAL", RENEW_SCRIPT, "1", prefix + eventId, token, String(ttlMs)])) === 1;
|
|
436
|
+
},
|
|
437
|
+
async release(eventId) {
|
|
438
|
+
const token = tokens.get(eventId);
|
|
439
|
+
tokens.delete(eventId);
|
|
440
|
+
if (token)
|
|
441
|
+
await command(["EVAL", RELEASE_SCRIPT, "1", prefix + eventId, token]);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function invalidToolInputResult(call) {
|
|
446
|
+
return {
|
|
447
|
+
error: "invalid_tool_input",
|
|
448
|
+
message: `The arguments for ${call.name} were not valid JSON (${call.invalid?.error ?? "unparseable"}). Call ${call.name} again with complete, valid JSON arguments.`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class EventReceiver {
|
|
453
|
+
client;
|
|
454
|
+
hooks;
|
|
455
|
+
options;
|
|
456
|
+
#lock;
|
|
457
|
+
constructor(client, hooks, options = {}) {
|
|
458
|
+
this.client = client;
|
|
459
|
+
this.hooks = hooks;
|
|
460
|
+
this.options = options;
|
|
461
|
+
this.#lock = options.lock ?? new MemoryEventLock;
|
|
462
|
+
}
|
|
463
|
+
async claim(event) {
|
|
464
|
+
const leaseMs = this.options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
465
|
+
if (!await this.#lock.acquire(event.id, leaseMs))
|
|
466
|
+
return null;
|
|
467
|
+
const hooks = this.hooks;
|
|
468
|
+
const load = typeof hooks === "function" ? () => hooks(event) : async () => hooks;
|
|
469
|
+
return (signal) => runClaimed(this.client, this.#lock, event, load, { ...this.options, signal: signal ?? this.options.signal });
|
|
434
470
|
}
|
|
435
471
|
}
|
|
472
|
+
var DEFAULT_LEASE_MS = 60000;
|
|
473
|
+
var DEFAULT_RETRY_DELAYS_MS = [1000, 3000, 9000];
|
|
436
474
|
|
|
437
475
|
class ClientTelemetry {
|
|
438
476
|
parentSpanId;
|
|
@@ -563,6 +601,10 @@ async function prepareResponseBody(event, hooks, telemetry, signal) {
|
|
|
563
601
|
return;
|
|
564
602
|
signal?.throwIfAborted();
|
|
565
603
|
const call = calls[index];
|
|
604
|
+
if (call.invalid) {
|
|
605
|
+
results[index] = { id: call.id, result: invalidToolInputResult(call), isError: true };
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
566
608
|
const handler = hooks.tools?.[call.name];
|
|
567
609
|
if (!handler)
|
|
568
610
|
throw new HookConfigurationError(`No handler for tool ${call.name}`);
|
|
@@ -619,44 +661,96 @@ async function prepareResponse(event, hooks, signal) {
|
|
|
619
661
|
response.telemetry = telemetry.snapshot();
|
|
620
662
|
return response;
|
|
621
663
|
}
|
|
622
|
-
async function processEvent(client,
|
|
623
|
-
|
|
664
|
+
async function processEvent(client, lock, event, hooks, options = {}) {
|
|
665
|
+
if (!await lock.acquire(event.id, options.leaseMs ?? DEFAULT_LEASE_MS))
|
|
666
|
+
return { status: "busy" };
|
|
667
|
+
return runClaimed(client, lock, event, async () => hooks, options);
|
|
668
|
+
}
|
|
669
|
+
function retryable(error) {
|
|
670
|
+
if (error instanceof PermanentEventError)
|
|
671
|
+
return false;
|
|
672
|
+
if (error instanceof ConstellationError) {
|
|
673
|
+
return !PERMANENT_RESPONSE_CODES.has(error.code) && (error.status === 0 || [408, 409, 425, 429].includes(error.status) || error.status >= 500);
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
function wait(ms, signal) {
|
|
678
|
+
signal.throwIfAborted();
|
|
679
|
+
return new Promise((resolve, reject) => {
|
|
680
|
+
const abort = () => {
|
|
681
|
+
clearTimeout(timer);
|
|
682
|
+
reject(signal.reason);
|
|
683
|
+
};
|
|
684
|
+
const timer = setTimeout(() => {
|
|
685
|
+
signal.removeEventListener("abort", abort);
|
|
686
|
+
resolve();
|
|
687
|
+
}, ms);
|
|
688
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async function runClaimed(client, lock, event, loadHooks, options) {
|
|
692
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
693
|
+
const delays = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
694
|
+
const fence = new AbortController;
|
|
695
|
+
const signal = options.signal ? AbortSignal.any([options.signal, fence.signal]) : fence.signal;
|
|
696
|
+
const renewal = setInterval(() => {
|
|
697
|
+
lock.renew(event.id, leaseMs).then((held) => {
|
|
698
|
+
if (!held)
|
|
699
|
+
fence.abort(new Error("Event lock lost"));
|
|
700
|
+
}, () => fence.abort(new Error("Event lock unavailable")));
|
|
701
|
+
}, Math.max(1000, Math.floor(leaseMs / 3)));
|
|
624
702
|
try {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (response) {
|
|
628
|
-
stage = "submit";
|
|
703
|
+
for (let attempt = 1;; attempt++) {
|
|
704
|
+
const progress = { stage: "prepare" };
|
|
629
705
|
try {
|
|
630
|
-
|
|
706
|
+
return await answer(client, event, await loadHooks(), signal, progress);
|
|
631
707
|
} catch (error) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
708
|
+
const willRetry = attempt <= delays.length && !signal.aborted && retryable(error);
|
|
709
|
+
if (options.onFailure) {
|
|
710
|
+
const detail = error instanceof ConstellationError ? { status: error.status, code: error.code, message: error.message } : { status: 0, code: error instanceof Error && error.name === "TimeoutError" ? "timeout" : progress.stage === "submit" ? "transport_error" : "event_prepare_failed", message: error instanceof Error ? error.message : "Event processing failed" };
|
|
711
|
+
try {
|
|
712
|
+
await options.onFailure(event.id, { ...detail, attempt, willRetry });
|
|
713
|
+
} catch (monitorError) {
|
|
714
|
+
if (error && typeof error === "object")
|
|
715
|
+
Object.assign(error, { failureHookError: monitorError });
|
|
716
|
+
}
|
|
636
717
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const failure = new HookConfigurationError(response.error.message);
|
|
641
|
-
await store.reject(event.id, failure);
|
|
642
|
-
throw failure;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
stage = "checkpoint";
|
|
646
|
-
await store.complete(event.id, receipt);
|
|
647
|
-
} catch (error) {
|
|
648
|
-
if (!(error instanceof PermanentEventError) && store.retry) {
|
|
649
|
-
const detail = error instanceof ConstellationError ? { status: error.status, code: error.code, message: error.message } : { status: 0, code: error instanceof Error && error.name === "TimeoutError" ? "timeout" : stage === "submit" ? "transport_error" : `event_${stage}_failed`, message: error instanceof Error ? error.message : "Event processing failed" };
|
|
650
|
-
try {
|
|
651
|
-
await store.retry(event.id, detail);
|
|
652
|
-
} catch (monitorError) {
|
|
653
|
-
if (error && typeof error === "object")
|
|
654
|
-
Object.assign(error, { retryHookError: monitorError });
|
|
718
|
+
if (!willRetry)
|
|
719
|
+
throw error;
|
|
720
|
+
await wait(delays[attempt - 1] ?? 0, signal);
|
|
655
721
|
}
|
|
656
722
|
}
|
|
657
|
-
|
|
723
|
+
} finally {
|
|
724
|
+
clearInterval(renewal);
|
|
725
|
+
await lock.release(event.id).catch(() => {});
|
|
658
726
|
}
|
|
659
727
|
}
|
|
728
|
+
async function answer(client, event, hooks, signal, progress) {
|
|
729
|
+
const response = await prepareResponse(event, hooks, signal);
|
|
730
|
+
if (!response)
|
|
731
|
+
return { status: "answered", action: "stop", receipt: null, ended: null };
|
|
732
|
+
progress.stage = "submit";
|
|
733
|
+
signal.throwIfAborted();
|
|
734
|
+
const observe = "action" in response && response.action === "observe";
|
|
735
|
+
let receipt;
|
|
736
|
+
try {
|
|
737
|
+
receipt = await client.requests.submit(response, signal);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (!(error instanceof ConstellationError) || !PERMANENT_RESPONSE_CODES.has(error.code) || observe)
|
|
740
|
+
throw error;
|
|
741
|
+
receipt = await client.requests.submit({
|
|
742
|
+
action: "observe",
|
|
743
|
+
idempotencyKey: event.id,
|
|
744
|
+
inReplyTo: { requestId: event.requestId, eventId: event.id },
|
|
745
|
+
telemetry: response.telemetry ?? { spans: [], logs: [] },
|
|
746
|
+
error: { code: error.code, message: error.message.slice(0, 8192) || error.code }
|
|
747
|
+
}, signal);
|
|
748
|
+
return { status: "answered", action: "stop", receipt, ended: receipt.outcome === "event_closed" ? "closed" : "failed" };
|
|
749
|
+
}
|
|
750
|
+
const failed = observe && "error" in response && !!response.error;
|
|
751
|
+
const ended = receipt.outcome === "event_closed" ? "closed" : observe ? failed ? "failed" : "completed" : null;
|
|
752
|
+
return { status: "answered", action: observe ? "stop" : "continue", receipt, ended };
|
|
753
|
+
}
|
|
660
754
|
|
|
661
755
|
// src/index.ts
|
|
662
756
|
function queryString2(values) {
|
|
@@ -807,12 +901,15 @@ class Constellation extends RequestClient {
|
|
|
807
901
|
}
|
|
808
902
|
export {
|
|
809
903
|
uuid7,
|
|
904
|
+
redisEventLock,
|
|
810
905
|
processEvent,
|
|
811
906
|
prepareResponse,
|
|
812
907
|
parseOpenerSentence,
|
|
813
908
|
normalizeTimestamp,
|
|
909
|
+
invalidToolInputResult,
|
|
814
910
|
PermanentEventError,
|
|
815
911
|
PERMANENT_RESPONSE_CODES,
|
|
912
|
+
MemoryEventLock,
|
|
816
913
|
HookConfigurationError,
|
|
817
914
|
EventReceiver,
|
|
818
915
|
ConstellationError,
|
|
@@ -10,34 +10,75 @@ export declare class PermanentEventError extends Error {
|
|
|
10
10
|
export declare class HookConfigurationError extends PermanentEventError {
|
|
11
11
|
constructor(message: string);
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
/** Exclusive, expiring ownership of one event across the application's processes.
|
|
14
|
+
* Constellation re-sends an event that stays unanswered; whoever holds the lock
|
|
15
|
+
* answers that re-send with 202 instead of starting a second run. */
|
|
16
|
+
export interface EventLock {
|
|
17
|
+
/** Take the event for `ttlMs`. False while another holder's lease is live. */
|
|
18
|
+
acquire(eventId: string, ttlMs: number): Promise<boolean>;
|
|
19
|
+
/** Extend this holder's lease. False if it expired or another holder took it. */
|
|
20
|
+
renew(eventId: string, ttlMs: number): Promise<boolean>;
|
|
21
|
+
release(eventId: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** Correct for a single process. Use `redisEventLock` when several processes receive events. */
|
|
24
|
+
export declare class MemoryEventLock implements EventLock {
|
|
25
|
+
#private;
|
|
26
|
+
acquire(eventId: string, ttlMs: number): Promise<boolean>;
|
|
27
|
+
renew(eventId: string, ttlMs: number): Promise<boolean>;
|
|
28
|
+
release(eventId: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** A lock shared through Redis. `command` sends one raw command and resolves its reply,
|
|
31
|
+
* for example `(args) => redis.call(...args)` (ioredis) or `(args) => client.sendCommand(args)` (node-redis). */
|
|
32
|
+
export declare function redisEventLock(command: (args: string[]) => Promise<unknown>, options?: {
|
|
33
|
+
prefix?: string;
|
|
34
|
+
}): EventLock;
|
|
35
|
+
/** What processing an event did. `ended` says the request is over after this response:
|
|
36
|
+
* `completed` or `failed` from this answer, `closed` when the request had already ended. */
|
|
37
|
+
export type EventOutcome = {
|
|
38
|
+
status: "busy";
|
|
39
|
+
} | {
|
|
40
|
+
status: "answered";
|
|
41
|
+
action: "continue" | "stop";
|
|
42
|
+
receipt: AdmissionReceipt | null;
|
|
43
|
+
ended: "completed" | "failed" | "closed" | null;
|
|
44
|
+
};
|
|
45
|
+
export type EventOptions = {
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
/** Lock lease, renewed every third of it while processing. Keep it well below the response window. */
|
|
48
|
+
leaseMs?: number;
|
|
49
|
+
/** Pauses before retrying a transient failure while still holding the event, before leaving
|
|
50
|
+
* it to Constellation's re-send. Default 1 s, 3 s and 9 s; `[]` leaves every failure to the re-send. */
|
|
51
|
+
retryDelaysMs?: readonly number[];
|
|
52
|
+
/** Reports each failed attempt. `willRetry` is false for the last one: the event stays
|
|
53
|
+
* unanswered and Constellation re-sends it after its response window. */
|
|
54
|
+
onFailure?: (eventId: string, error: {
|
|
31
55
|
status: number;
|
|
32
56
|
code: string;
|
|
33
57
|
message: string;
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
attempt: number;
|
|
59
|
+
willRetry: boolean;
|
|
60
|
+
}) => void | Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
/** The error result returned to the model for a call whose arguments were not valid JSON. */
|
|
63
|
+
export declare function invalidToolInputResult(call: {
|
|
64
|
+
name: string;
|
|
65
|
+
invalid?: {
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
68
|
+
}): ToolResult["result"];
|
|
69
|
+
/** Webhook-side processing. After authenticating an event, call `claim`: `null` means another
|
|
70
|
+
* process holds it (answer 202); otherwise answer 204 and run the returned function in the
|
|
71
|
+
* background. Tools never run inside the webhook call. */
|
|
36
72
|
export declare class EventReceiver {
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
#private;
|
|
74
|
+
private readonly client;
|
|
75
|
+
private readonly hooks;
|
|
76
|
+
private readonly options;
|
|
77
|
+
/** `hooks` may be loaded per event, for example to bind them to the request's own records. */
|
|
78
|
+
constructor(client: RequestClient, hooks: AgentHooks | ((event: WebhookEvent) => Promise<AgentHooks>), options?: EventOptions & {
|
|
79
|
+
lock?: EventLock;
|
|
80
|
+
});
|
|
81
|
+
claim(event: WebhookEvent): Promise<((signal?: AbortSignal) => Promise<EventOutcome>) | null>;
|
|
41
82
|
}
|
|
42
83
|
export declare class ClientTelemetry {
|
|
43
84
|
readonly parentSpanId?: string | undefined;
|
|
@@ -75,7 +116,6 @@ export interface AgentHooks {
|
|
|
75
116
|
}
|
|
76
117
|
/** Record the client processing interval and its actual child-span hierarchy. */
|
|
77
118
|
export declare function prepareResponse(event: WebhookEvent, hooks: AgentHooks, signal?: AbortSignal): Promise<Admission | null>;
|
|
78
|
-
/** Process an authenticated event
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
export declare function processEvent(client: RequestClient, store: EventStore, event: WebhookEvent, hooks: AgentHooks, signal?: AbortSignal): Promise<void>;
|
|
119
|
+
/** Process an authenticated event inline: take its lock, prepare and submit the response, release.
|
|
120
|
+
* Returns `busy` without doing anything while another process holds the event. */
|
|
121
|
+
export declare function processEvent(client: RequestClient, lock: EventLock, event: WebhookEvent, hooks: AgentHooks, options?: EventOptions): Promise<EventOutcome>;
|
|
@@ -7,10 +7,17 @@ export type Tool = {
|
|
|
7
7
|
description?: string;
|
|
8
8
|
inputSchema: Json;
|
|
9
9
|
};
|
|
10
|
+
/** The provider's tool arguments were not valid JSON. `raw` is truncated to 16 KiB. */
|
|
11
|
+
export type InvalidToolInput = {
|
|
12
|
+
raw: string;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/** With `invalid` set, `arguments` is `{}`: answer with an error result instead of running the tool. */
|
|
10
16
|
export type ToolCall = {
|
|
11
17
|
id: string;
|
|
12
18
|
name: string;
|
|
13
19
|
arguments: Json;
|
|
20
|
+
invalid?: InvalidToolInput;
|
|
14
21
|
};
|
|
15
22
|
export type Message = {
|
|
16
23
|
role: "system" | "user" | "assistant" | "tool";
|
|
@@ -122,6 +129,9 @@ export type AdmissionReceipt = {
|
|
|
122
129
|
outcome?: ResponseOutcome;
|
|
123
130
|
/** With `event_closed`: whether the continuation's telemetry was kept as late observations. */
|
|
124
131
|
observationsRecorded?: boolean;
|
|
132
|
+
/** With `already_accepted`: false when this responder answered the event before with a
|
|
133
|
+
* different body (for example recomputed after a lost acknowledgement). The first answer stands. */
|
|
134
|
+
bodyMatched?: boolean;
|
|
125
135
|
};
|
|
126
136
|
export type Status = "queued" | "running" | "client_processing" | "completed" | "failed" | "cancelled";
|
|
127
137
|
export type RequestRecord = {
|
|
@@ -349,6 +359,14 @@ export type ModelResolution = ModelScope & {
|
|
|
349
359
|
headers?: Record<string, string>;
|
|
350
360
|
nativeContentPolicy?: "require" | "omitCompleted" | "omitIncompatible";
|
|
351
361
|
};
|
|
362
|
+
/** Whose request a boundary event belongs to, so a receiver never reads the request back. */
|
|
363
|
+
export type EventRequest = {
|
|
364
|
+
id: string;
|
|
365
|
+
externalId: string | null;
|
|
366
|
+
workspaceId: string;
|
|
367
|
+
userId: string | null;
|
|
368
|
+
idempotencyKey: string;
|
|
369
|
+
};
|
|
352
370
|
export type ModelCompletedEvent = {
|
|
353
371
|
id: string;
|
|
354
372
|
requestId: string;
|
|
@@ -357,10 +375,14 @@ export type ModelCompletedEvent = {
|
|
|
357
375
|
type: "model.completed";
|
|
358
376
|
data: {
|
|
359
377
|
message: Message;
|
|
378
|
+
/** Step result: `stepId`, `stepInput` (`model`, `parameters`, `maxOutputTokens`, `timeoutMs`, `toolsBytes`),
|
|
379
|
+
* `usage`, costs, `finishReason` and the step's `attempts`. */
|
|
360
380
|
result: Record<string, Json>;
|
|
381
|
+
/** 1-based number of the step this event completes. */
|
|
382
|
+
stepNumber: number;
|
|
383
|
+
request: EventRequest;
|
|
361
384
|
events?: Json[];
|
|
362
385
|
stepId?: string;
|
|
363
|
-
stepNumber?: number;
|
|
364
386
|
attemptId?: string;
|
|
365
387
|
};
|
|
366
388
|
};
|
|
@@ -370,7 +392,10 @@ export type WebhookEvent = ModelCompletedEvent | {
|
|
|
370
392
|
sequence: number;
|
|
371
393
|
createdAt: string;
|
|
372
394
|
type: "model.failed" | "model.cancelled";
|
|
373
|
-
data: Record<string, Json
|
|
395
|
+
data: Record<string, Json> & {
|
|
396
|
+
stepNumber: number;
|
|
397
|
+
request: EventRequest;
|
|
398
|
+
};
|
|
374
399
|
};
|
|
375
400
|
export declare class ConstellationError extends Error {
|
|
376
401
|
status: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** External server SDK. Operator APIs and cookie sessions are internal-only clients. */
|
|
2
2
|
import { RequestClient, type WebhookEvent, type Json, type Page, type PageQuery, type ModelResolution, type RequestRecord, type RequestEvent, type RecordTypes, type Timeline, type TimelineRow, type RequestListQuery, type RequestRecordsQuery, type RequestTimings, type TokenUsage } from "../../client-core/src/index";
|
|
3
3
|
export { ConstellationError, uuid7, normalizeTimestamp, parseOpenerSentence } from "../../client-core/src/index";
|
|
4
|
-
export type { InferenceParameters, Json, Tool, ToolCall, Message, RequestEventModes, RequestInput, Admission, AdmissionReceipt, ResponseOutcome, ObservationResponse, Observations, EventReference, ModelCompletedEvent, ModelDeltaEvent, ModelStreamFrame, RequestTimings, TimingInterval, TokenUsage, TokenPrice, CustomerPricing, BillingFacts, Status, RequestRecord, Step, Attempt, Span, TimelineRow, Timeline, Log, Inspection, RecordTypes, RequestEvent, Page, PageQuery, ModelScope, ModelResolution, WebhookEvent, RequestRecordsQuery, RequestListQuery, OpenerInput, OpenerTurn } from "../../client-core/src/index";
|
|
4
|
+
export type { InferenceParameters, Json, Tool, ToolCall, InvalidToolInput, EventRequest, Message, RequestEventModes, RequestInput, Admission, AdmissionReceipt, ResponseOutcome, ObservationResponse, Observations, EventReference, ModelCompletedEvent, ModelDeltaEvent, ModelStreamFrame, RequestTimings, TimingInterval, TokenUsage, TokenPrice, CustomerPricing, BillingFacts, Status, RequestRecord, Step, Attempt, Span, TimelineRow, Timeline, Log, Inspection, RecordTypes, RequestEvent, Page, PageQuery, ModelScope, ModelResolution, WebhookEvent, RequestRecordsQuery, RequestListQuery, OpenerInput, OpenerTurn } from "../../client-core/src/index";
|
|
5
5
|
/** OpenAPI contract filtered to this key's permissions and scope. */
|
|
6
6
|
export type DiscoveryDocument = {
|
|
7
7
|
openapi: string;
|
|
@@ -263,5 +263,5 @@ export declare class Constellation extends RequestClient {
|
|
|
263
263
|
verify: (request: Request, expectedToken: string) => Promise<WebhookEvent>;
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
-
export { PermanentEventError, HookConfigurationError, EventReceiver, prepareResponse, processEvent, ClientTelemetry, PERMANENT_RESPONSE_CODES } from "../../client-core/src/agent";
|
|
267
|
-
export type {
|
|
266
|
+
export { PermanentEventError, HookConfigurationError, EventReceiver, MemoryEventLock, redisEventLock, prepareResponse, processEvent, ClientTelemetry, PERMANENT_RESPONSE_CODES, invalidToolInputResult } from "../../client-core/src/agent";
|
|
267
|
+
export type { EventLock, EventOutcome, EventOptions, AgentHooks, HookContext, ToolResult } from "../../client-core/src/agent";
|