@lionhummer/constellation 0.9.2 → 0.10.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 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 four pieces:
73
+ A tool-using agent needs three pieces:
74
74
 
75
- 1. **A webhook** that receives events and stores them.
75
+ 1. **A webhook** that receives events.
76
76
  2. **Hooks** that run your tools and build the next input.
77
- 3. **An `EventStore`**: a table in your database that remembers each event and the response you prepared for it.
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. Implement the `EventStore`
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
- ### 4. Receive webhooks and process events
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 { processEvent, PermanentEventError } from "@lionhummer/constellation";
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 }, "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,60 @@ export async function webhook(request: Request): Promise<Response> {
256
162
  } catch {
257
163
  return new Response(null, { status: 401 });
258
164
  }
259
- await store.accept(event); // if this throws, the 5xx makes Constellation redeliver
260
- void handle(event); // don't make the webhook wait for your tools
261
- return new Response(null, { status: 202 });
262
- }
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
- }
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 });
274
171
  }
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
- If your process dies while handling an event, you don't need a poller to recover it. Constellation sends an unanswered `model.completed` event again after the response window (10 minutes by default), up to 3 times, and stops as soon as your response arrives. A re-sent event has the same `id`, so `store.accept` and `store.prepare` resume it from your saved checkpoint. If the event is still being processed when the re-send arrives, acknowledge it anyway. Keep your processing lease shorter than the response window and renew it while work continues, so a re-send after a crash can take the event over.
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
- ## What `processEvent` does
178
+ ### Recovery
285
179
 
286
- For each event:
180
+ 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`:
287
181
 
288
- 1. **Prepare the response, at most once.** `store.prepare` returns the saved response if one exists. Otherwise the SDK builds one:
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
+ - 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;
183
+ - 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.
291
184
 
292
- The response carries the idempotency key `event.id`, `inReplyTo: { requestId, eventId }` and the telemetry recorded while preparing. The store saves it before anything is sent.
293
- 2. **Submit it** through `requests.submit`. Retries send the saved body unchanged.
294
- 3. **Record the outcome**:
185
+ 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.
295
186
 
296
- | Server answer | Meaning | `processEvent` |
297
- | --- | --- | --- |
298
- | `200`, `outcome: "accepted"` | Your response was accepted. A continuation starts the next step (`scheduled: true`). | `store.complete`, resolves |
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
+ ## What happens to each event
188
+
189
+ 1. **Take the lock** for the event. If another process holds it: `busy` (answer `202`).
190
+ 2. **Prepare the response:**
191
+ - 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.
192
+ - 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.
193
+ - For `model.failed` and `model.cancelled`, call `onError(event.data)` and produce an `observe` response.
305
194
 
306
- The permanent codes are exported as `PERMANENT_RESPONSE_CODES`. The receipt passed to `store.complete` includes `outcome`, so your store can record that a request ended without continuing.
195
+ The response carries the idempotency key `event.id`, `inReplyTo: { requestId, eventId }` and the telemetry recorded while preparing.
196
+ 3. **Submit it** and release the lock:
307
197
 
308
- Fix `401` and `403` by correcting the key: the saved response is still sent unchanged afterwards.
198
+ | Server answer | Meaning | Result |
199
+ | --- | --- | --- |
200
+ | `200`, `outcome: "accepted"` | Your response was accepted. A continuation starts the next step (`scheduled: true`). | `answered` |
201
+ | `200`, `outcome: "already_accepted"` | This event was already answered by you. `bodyMatched: false` means the earlier answer differed and stands. | `answered` |
202
+ | `200`, `outcome: "event_closed"` | The request was cancelled or expired before your continuation arrived. Your telemetry is still recorded (`observationsRecorded`). | `answered`, `ended: "closed"` |
203
+ | 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"` |
204
+ | Anything else: network error, timeout, `5xx`, `429`, `409 conflict`, `401`, `403` | Temporary. | `onFailure` runs and the call throws. The event stays unanswered and is re-sent. |
205
+
206
+ 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
207
 
310
208
  ## When your code fails
311
209
 
312
210
  | What happened | Without `onError` | With `onError` |
313
211
  | --- | --- | --- |
314
- | A tool or hook throws | `processEvent` throws and the event is retried later. 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 retried. |
315
- | No handler for a tool the model called, or `prepareRequest` is missing when continuing | The request is marked **failed** and `HookConfigurationError` (a `PermanentEventError`) is thrown. | `onError` runs, as above. |
212
+ | 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. |
213
+ | 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
214
  | The model step failed or was cancelled (`model.failed`, `model.cancelled`) | An `observe` response records your telemetry. | `onError(event.data)` runs first. |
317
215
 
318
216
  ## Side effects and duplicates
319
217
 
320
- Hooks can run more than once. A crash after a tool ran, but before the prepared response was saved, means the tool runs again. Constellation cannot make your side effects exactly-once; your code must:
218
+ 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
219
 
322
220
  - pass `effectId` to anything with side effects (payments, emails, writes). It is stable for one tool call of one event;
323
221
  - settle per-step billing or usage once per `event.id`;
@@ -341,6 +239,8 @@ tools: {
341
239
  },
342
240
  ```
343
241
 
242
+ Work you do before the first model step, such as loading context and building the prompt, can travel with the request that starts the run: pass `telemetry: { spans, logs }` to `requests.submit`. Those spans belong to the request rather than a step: they sit at the top of the timeline at their own timestamps, before the first step (set `stepId` or `parentSpanId` to nest them elsewhere).
243
+
344
244
  The SDK records `client.processing` and one span per tool automatically. A response carries at most 512 spans, 128 logs and 1 MiB of telemetry; the SDK trims larger batches and records what it omitted. Do not put secrets or personal data in span names, attributes or log messages.
345
245
 
346
246
  ## Opening lines
@@ -361,6 +261,45 @@ if (opener) void client.openers.sentence(opener.id).then((sentence) => {
361
261
 
362
262
  Start the real request without waiting for the opener. `parseOpenerSentence(text)` applies the same checks to text you already have.
363
263
 
264
+ ## Telemetry queries
265
+
266
+ Ask aggregate questions without SQL. Fields, filters and metrics are typed per dataset.
267
+
268
+ ```ts
269
+ // How many fireworks-direct attempts failed yesterday, and why?
270
+ const failures = await client.telemetry.query({
271
+ dataset: "attempts",
272
+ from: "2026-10-15T00:00:00Z", to: "2026-10-16T00:00:00Z",
273
+ where: { offeringId: { startsWith: "fireworks-direct:" }, status: "failed" },
274
+ groupBy: ["errorCode", "httpStatus"],
275
+ metrics: ["count", "uniq:requestId"],
276
+ })
277
+
278
+ // How often did a provider say "Response too long", per day?
279
+ const mentions = await client.telemetry.query({
280
+ dataset: "attempts",
281
+ from: "2026-10-01T00:00:00Z", to: "2026-10-16T00:00:00Z",
282
+ where: { errorMessage: { contains: "Response too long" } },
283
+ groupBy: ["day"],
284
+ })
285
+ ```
286
+
287
+ Usage and costs come from the permanent billing record (`usage`, one row per settled request, or `usageAttempts`, one row per provider attempt for provider/model splits). Any range works:
288
+
289
+ ```ts
290
+ // Customer charges, supplier cost and margin per workspace and provider last month, without QA
291
+ const byProvider = await client.telemetry.query({
292
+ dataset: "usageAttempts",
293
+ from: "2026-09-01T00:00:00Z", to: "2026-10-01T00:00:00Z",
294
+ where: { workspaceId: { notIn: ["qa"] } },
295
+ groupBy: ["workspaceId", "providerId"],
296
+ metrics: ["uniq:requestId", "sum:customerCost", "sum:supplierCost", "sum:margin"],
297
+ limit: 10000,
298
+ })
299
+ ```
300
+
301
+ A scoped key only ever sees its workspace and user. Logs and usage need an unscoped key. Telemetry ranges are at most 92 days and results at most 1,000 rows; usage allows any range and 10,000 rows. Group by `minute`, `hour`, `day`, `week` or `month` for time series.
302
+
364
303
  ## Limits and timeouts
365
304
 
366
305
  | Setting | Default | Notes |
@@ -386,7 +325,7 @@ A failed provider call falls back to the next provider only before any output ha
386
325
  | `requests.preview(input)` | Validate a complete input and show routing, without calling a provider. |
387
326
  | `requests.cancel(id)` | Cancel a request. |
388
327
  | `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). |
389
- | `requests.getState(id)` | Committed state: status, steps, attempt outcomes, usage and cost. |
328
+ | `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. |
390
329
  | `requests.getSummary(id)`, `requests.get(id)` | Request detail, without or with spans and logs. |
391
330
  | `requests.list(query)` | Page through requests, e.g. by `externalId` or `status`. |
392
331
  | `requests.listEvents(id, after)`, `requests.streamEvents(id)` | Replay events after a sequence number, or stream them live. |
@@ -394,11 +333,12 @@ A failed provider call falls back to the next provider only before any output ha
394
333
  | `requests.getTimeline(id)`, `requests.getTimings(id)` | Trace and timing breakdown. |
395
334
  | `requests.getHttpExchange(id, attemptId)`, `requests.getHttpBody(...)` | The provider HTTP call captured for one attempt. |
396
335
  | `webhooks.verify(request, token)` | Authenticate and parse an incoming webhook. |
336
+ | `telemetry.query(query)` | Count, group and aggregate attempts, requests, logs or permanent usage, e.g. failed attempts per offering and error code, or cost per workspace and provider. Needs telemetry permission; the key's scope always applies. |
397
337
  | `telemetry.*` | Read-only reporting. Returns metadata only unless you pass `include: "content"`. Needs telemetry permission. |
398
338
  | `management.registry.*` | Edit the model registry. Needs management permission. |
399
339
  | `discover()` | The OpenAPI document for exactly what this key may call. |
400
340
 
401
- Exports for event processing: `processEvent`, `prepareResponse` (build a response without submitting it), `EventReceiver`, `ClientTelemetry`, `PermanentEventError`, `HookConfigurationError`, `PERMANENT_RESPONSE_CODES`, `ConstellationError` and `uuid7`.
341
+ 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`.
402
342
 
403
343
  ## Troubleshooting
404
344
 
@@ -406,7 +346,7 @@ Exports for event processing: `processEvent`, `prepareResponse` (build a respons
406
346
  | --- | --- |
407
347
  | `models.list` is empty | The key's workspace/user scope, an access rule for that workspace, and an enabled model and offering. |
408
348
  | `terminal: "notify"` is rejected | The execution key has no webhook URL configured. |
409
- | The same webhook keeps arriving | Your endpoint returned non-2xx or took longer than the delivery timeout (5 s by default). Return 202 after `store.accept` and process in the background. |
349
+ | 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. |
410
350
  | `400` on a continuation | Model, workspace, user, `externalId`, plan, events or `timeoutMs` differ from the first request, or the message history is malformed. |
411
351
  | `409 response_conflict` | Your code produced a different response for an event that was already answered. Answers must come from the saved response. |
412
352
  | Tools ran twice | Expected after a crash. Make side effects idempotent with `effectId`. |
package/dist/index.js CHANGED
@@ -424,15 +424,88 @@ class HookConfigurationError extends PermanentEventError {
424
424
  }
425
425
  }
426
426
 
427
- class EventReceiver {
428
- store;
429
- constructor(store) {
430
- this.store = store;
427
+ class MemoryEventLock {
428
+ #held = new Map;
429
+ async acquire(eventId, ttlMs) {
430
+ const until = this.#held.get(eventId);
431
+ if (until !== undefined && until > Date.now())
432
+ return false;
433
+ this.#held.set(eventId, Date.now() + ttlMs);
434
+ return true;
435
+ }
436
+ async renew(eventId, ttlMs) {
437
+ if (!this.#held.has(eventId))
438
+ return false;
439
+ this.#held.set(eventId, Date.now() + ttlMs);
440
+ return true;
441
+ }
442
+ async release(eventId) {
443
+ this.#held.delete(eventId);
431
444
  }
432
- async receive(event) {
433
- await this.store.accept(event);
445
+ }
446
+ var RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
447
+ var RELEASE_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
448
+ function redisEventLock(command, options = {}) {
449
+ const prefix = options.prefix ?? "constellation:event-lock:";
450
+ const tokens = new Map;
451
+ return {
452
+ async acquire(eventId, ttlMs) {
453
+ const token = uuid7();
454
+ const reply = await command(["SET", prefix + eventId, token, "NX", "PX", String(ttlMs)]);
455
+ if (reply !== "OK")
456
+ return false;
457
+ tokens.set(eventId, token);
458
+ return true;
459
+ },
460
+ async renew(eventId, ttlMs) {
461
+ const token = tokens.get(eventId);
462
+ if (!token)
463
+ return false;
464
+ return Number(await command(["EVAL", RENEW_SCRIPT, "1", prefix + eventId, token, String(ttlMs)])) === 1;
465
+ },
466
+ async release(eventId) {
467
+ const token = tokens.get(eventId);
468
+ tokens.delete(eventId);
469
+ if (token)
470
+ await command(["EVAL", RELEASE_SCRIPT, "1", prefix + eventId, token]);
471
+ }
472
+ };
473
+ }
474
+ function invalidToolInputResult(call) {
475
+ return {
476
+ error: "invalid_tool_input",
477
+ message: `The arguments for ${call.name} were not valid JSON (${call.invalid?.error ?? "unparseable"}). Call ${call.name} again with complete, valid JSON arguments.`
478
+ };
479
+ }
480
+
481
+ class EventReceiver {
482
+ client;
483
+ hooks;
484
+ options;
485
+ #lock;
486
+ constructor(client, hooks, options = {}) {
487
+ this.client = client;
488
+ this.hooks = hooks;
489
+ this.options = options;
490
+ this.#lock = options.lock ?? new MemoryEventLock;
491
+ }
492
+ async claim(event) {
493
+ const leaseMs = this.options.leaseMs ?? DEFAULT_LEASE_MS;
494
+ if (!await this.#lock.acquire(event.id, leaseMs))
495
+ return null;
496
+ return async (signal) => {
497
+ let hooks;
498
+ try {
499
+ hooks = typeof this.hooks === "function" ? await this.hooks(event) : this.hooks;
500
+ } catch (error) {
501
+ await this.#lock.release(event.id).catch(() => {});
502
+ throw error;
503
+ }
504
+ return runClaimed(this.client, this.#lock, event, hooks, { ...this.options, signal: signal ?? this.options.signal });
505
+ };
434
506
  }
435
507
  }
508
+ var DEFAULT_LEASE_MS = 60000;
436
509
 
437
510
  class ClientTelemetry {
438
511
  parentSpanId;
@@ -563,6 +636,10 @@ async function prepareResponseBody(event, hooks, telemetry, signal) {
563
636
  return;
564
637
  signal?.throwIfAborted();
565
638
  const call = calls[index];
639
+ if (call.invalid) {
640
+ results[index] = { id: call.id, result: invalidToolInputResult(call), isError: true };
641
+ continue;
642
+ }
566
643
  const handler = hooks.tools?.[call.name];
567
644
  if (!handler)
568
645
  throw new HookConfigurationError(`No handler for tool ${call.name}`);
@@ -619,42 +696,61 @@ async function prepareResponse(event, hooks, signal) {
619
696
  response.telemetry = telemetry.snapshot();
620
697
  return response;
621
698
  }
622
- async function processEvent(client, store, event, hooks, signal) {
699
+ async function processEvent(client, lock, event, hooks, options = {}) {
700
+ if (!await lock.acquire(event.id, options.leaseMs ?? DEFAULT_LEASE_MS))
701
+ return { status: "busy" };
702
+ return runClaimed(client, lock, event, hooks, options);
703
+ }
704
+ async function runClaimed(client, lock, event, hooks, options) {
705
+ const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
706
+ const fence = new AbortController;
707
+ const signal = options.signal ? AbortSignal.any([options.signal, fence.signal]) : fence.signal;
708
+ const renewal = setInterval(() => {
709
+ lock.renew(event.id, leaseMs).then((held) => {
710
+ if (!held)
711
+ fence.abort(new Error("Event lock lost"));
712
+ }, () => fence.abort(new Error("Event lock unavailable")));
713
+ }, Math.max(1000, Math.floor(leaseMs / 3)));
623
714
  let stage = "prepare";
624
715
  try {
625
- const response = await store.prepare(event, () => prepareResponse(event, hooks, signal));
626
- let receipt = null;
627
- if (response) {
628
- stage = "submit";
629
- try {
630
- receipt = await client.requests.submit(response, signal);
631
- } catch (error) {
632
- if (error instanceof ConstellationError && PERMANENT_RESPONSE_CODES.has(error.code)) {
633
- const rejection = new PermanentEventError(error.status, error.code, error.message);
634
- await store.reject(event.id, rejection);
635
- throw rejection;
636
- }
716
+ const response = await prepareResponse(event, hooks, signal);
717
+ if (!response)
718
+ return { status: "answered", action: "stop", receipt: null, ended: null };
719
+ stage = "submit";
720
+ signal.throwIfAborted();
721
+ const observe = "action" in response && response.action === "observe";
722
+ let receipt;
723
+ try {
724
+ receipt = await client.requests.submit(response, signal);
725
+ } catch (error) {
726
+ if (!(error instanceof ConstellationError) || !PERMANENT_RESPONSE_CODES.has(error.code) || observe)
637
727
  throw error;
638
- }
639
- if ("action" in response && response.action === "observe" && response.error) {
640
- const failure = new HookConfigurationError(response.error.message);
641
- await store.reject(event.id, failure);
642
- throw failure;
643
- }
728
+ receipt = await client.requests.submit({
729
+ action: "observe",
730
+ idempotencyKey: event.id,
731
+ inReplyTo: { requestId: event.requestId, eventId: event.id },
732
+ telemetry: response.telemetry ?? { spans: [], logs: [] },
733
+ error: { code: error.code, message: error.message.slice(0, 8192) || error.code }
734
+ }, signal);
735
+ return { status: "answered", action: "stop", receipt, ended: receipt.outcome === "event_closed" ? "closed" : "failed" };
644
736
  }
645
- stage = "checkpoint";
646
- await store.complete(event.id, receipt);
737
+ const failed = observe && "error" in response && !!response.error;
738
+ const ended = receipt.outcome === "event_closed" ? "closed" : observe ? failed ? "failed" : "completed" : null;
739
+ return { status: "answered", action: observe ? "stop" : "continue", receipt, ended };
647
740
  } 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" };
741
+ if (options.onFailure) {
742
+ 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_prepare_failed", message: error instanceof Error ? error.message : "Event processing failed" };
650
743
  try {
651
- await store.retry(event.id, detail);
744
+ await options.onFailure(event.id, detail);
652
745
  } catch (monitorError) {
653
746
  if (error && typeof error === "object")
654
- Object.assign(error, { retryHookError: monitorError });
747
+ Object.assign(error, { failureHookError: monitorError });
655
748
  }
656
749
  }
657
750
  throw error;
751
+ } finally {
752
+ clearInterval(renewal);
753
+ await lock.release(event.id).catch(() => {});
658
754
  }
659
755
  }
660
756
 
@@ -764,6 +860,10 @@ class Constellation extends RequestClient {
764
860
  get: (id, query = {}, signal) => this.#call(`/v1/telemetry/notifications/${encodeURIComponent(id)}${queryString2(query)}`, "GET", undefined, signal),
765
861
  listEvents: (query = {}, signal) => this.#call(`/v1/telemetry/notifications/events${queryString2(query)}`, "GET", undefined, signal)
766
862
  },
863
+ query: (query, signal) => {
864
+ const iso = (value) => value instanceof Date ? value.toISOString() : value;
865
+ return this.#call("/v1/telemetry/query", "POST", { ...query, from: iso(query.from), to: iso(query.to) }, signal);
866
+ },
767
867
  logs: {
768
868
  searchSystem: (query = {}, signal) => this.#call(`/v1/telemetry/logs/system${queryString2(query)}`, "GET", undefined, signal),
769
869
  searchRequests: (query = {}, signal) => this.#call(`/v1/telemetry/logs/requests${queryString2(query)}`, "GET", undefined, signal),
@@ -803,12 +903,15 @@ class Constellation extends RequestClient {
803
903
  }
804
904
  export {
805
905
  uuid7,
906
+ redisEventLock,
806
907
  processEvent,
807
908
  prepareResponse,
808
909
  parseOpenerSentence,
809
910
  normalizeTimestamp,
911
+ invalidToolInputResult,
810
912
  PermanentEventError,
811
913
  PERMANENT_RESPONSE_CODES,
914
+ MemoryEventLock,
812
915
  HookConfigurationError,
813
916
  EventReceiver,
814
917
  ConstellationError,
@@ -10,34 +10,69 @@ export declare class PermanentEventError extends Error {
10
10
  export declare class HookConfigurationError extends PermanentEventError {
11
11
  constructor(message: string);
12
12
  }
13
- /** Application-owned durable event checkpoints. All methods must survive process restart. */
14
- export interface EventStore {
15
- /** Atomically retain the event before processing it.
16
- * Duplicate IDs must compare equal payloads. Resolve only after durable receipt. */
17
- accept(event: WebhookEvent): Promise<void>;
18
- /** Serialize processing per event. Return the saved response if present; otherwise
19
- * run prepare and durably store its exact result before returning. A crash can
20
- * rerun prepare: tool/billing effects must use the supplied stable identities. */
21
- prepare(event: WebhookEvent, prepare: () => Promise<Admission | null>): Promise<Admission | null>;
22
- /** Atomically mark the event processed after its saved response settled. `receipt.outcome` is
23
- * `accepted`, `already_accepted` or `event_closed`; for `event_closed`, `receipt.observationsRecorded`
24
- * says whether the continuation's telemetry was kept. `null` when no response was submitted. */
25
- complete(eventId: string, receipt: AdmissionReceipt | null): Promise<void>;
26
- /** Durably stop retries of a rejected saved response. Replays must rethrow this rejection. */
27
- reject(eventId: string, error: PermanentEventError): Promise<void>;
28
- /** Persist retry count, first/last failure timestamps and latest error in the existing inbox. Non-HTTP failures use status 0; alert on age/repeated failures.
29
- * If this hook throws, processEvent still rethrows the original failure, with the hook error as its `retryHookError` property. */
30
- retry?(eventId: string, error: {
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
+ /** Reports a failure that leaves the event unanswered; Constellation will re-send it. */
50
+ onFailure?: (eventId: string, error: {
31
51
  status: number;
32
52
  code: string;
33
53
  message: string;
34
- }): Promise<void>;
35
- }
54
+ }) => void | Promise<void>;
55
+ };
56
+ /** The error result returned to the model for a call whose arguments were not valid JSON. */
57
+ export declare function invalidToolInputResult(call: {
58
+ name: string;
59
+ invalid?: {
60
+ error: string;
61
+ };
62
+ }): ToolResult["result"];
63
+ /** Webhook-side processing. After authenticating an event, call `claim`: `null` means another
64
+ * process holds it (answer 202); otherwise answer 204 and run the returned function in the
65
+ * background. Tools never run inside the webhook call. */
36
66
  export declare class EventReceiver {
37
- private readonly store;
38
- constructor(store: EventStore);
39
- /** Call after webhook authentication. Return a successful HTTP response only after this resolves. */
40
- receive(event: WebhookEvent): Promise<void>;
67
+ #private;
68
+ private readonly client;
69
+ private readonly hooks;
70
+ private readonly options;
71
+ /** `hooks` may be loaded per event, for example to bind them to the request's own records. */
72
+ constructor(client: RequestClient, hooks: AgentHooks | ((event: WebhookEvent) => Promise<AgentHooks>), options?: EventOptions & {
73
+ lock?: EventLock;
74
+ });
75
+ claim(event: WebhookEvent): Promise<((signal?: AbortSignal) => Promise<EventOutcome>) | null>;
41
76
  }
42
77
  export declare class ClientTelemetry {
43
78
  readonly parentSpanId?: string | undefined;
@@ -75,7 +110,6 @@ export interface AgentHooks {
75
110
  }
76
111
  /** Record the client processing interval and its actual child-span hierarchy. */
77
112
  export declare function prepareResponse(event: WebhookEvent, hooks: AgentHooks, signal?: AbortSignal): Promise<Admission | null>;
78
- /** Process an authenticated event using an application-owned durable checkpoint.
79
- * Any receipt (`accepted`, `already_accepted`, `event_closed`) completes the checkpoint; a permanent
80
- * error code rejects it with PermanentEventError; anything else reports through `store.retry` and throws. */
81
- export declare function processEvent(client: RequestClient, store: EventStore, event: WebhookEvent, hooks: AgentHooks, signal?: AbortSignal): Promise<void>;
113
+ /** Process an authenticated event inline: take its lock, prepare and submit the response, release.
114
+ * Returns `busy` without doing anything while another process holds the event. */
115
+ 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;
@@ -28,6 +28,63 @@ export type TelemetryPage<T> = Page<T> & {
28
28
  export type TelemetryQuery = {
29
29
  include?: "content";
30
30
  };
31
+ /** Fields of each aggregate dataset, as POST /v1/telemetry/query accepts them. */
32
+ export type AttemptField = "workspaceId" | "userId" | "externalId" | "requestId" | "providerId" | "offeringId" | "model" | "requestedModel" | "status" | "fallback" | "dispatched" | "errorCode" | "faultClass" | "httpStatus" | "errorMessage";
33
+ export type AttemptNumber = "ttftMs" | "latencyMs" | "outputTokens" | "supplierCost" | "customerCost" | "httpStatus";
34
+ export type RequestField = "workspaceId" | "userId" | "externalId" | "requestId" | "requestedModel" | "status";
35
+ export type RequestNumber = "durationMs" | "supplierCost" | "customerCost";
36
+ export type LogField = "level" | "name" | "status" | "requestId" | "message";
37
+ /** Permanent billing record per settled request (unscoped telemetry keys only). */
38
+ export type UsageField = "workspaceId" | "userId" | "externalId" | "requestId" | "requestedModel" | "planId" | "status";
39
+ type UsageTotals = "supplierCost" | "customerCost" | "margin" | "inputTokens" | "cacheReadTokens" | "cacheWriteTokens" | "cacheWriteOneHourTokens" | "outputTokens" | "reasoningTokens";
40
+ export type UsageNumber = UsageTotals | "durationMs";
41
+ /** Permanent billing record per provider attempt; `status` is the attempt's, `requestStatus` the request's. */
42
+ export type UsageAttemptField = Exclude<UsageField, "status"> | "requestStatus" | "providerId" | "model" | "providerModel" | "status";
43
+ export type UsageAttemptNumber = UsageTotals;
44
+ export type AggregateTimeGroup = "minute" | "hour" | "day" | "week" | "month";
45
+ export type AggregateScalar = string | number | boolean;
46
+ /** A value, any of several values, or operators combined with AND. `contains` is case-insensitive. */
47
+ export type AggregateFilter = AggregateScalar | AggregateScalar[] | {
48
+ eq?: AggregateScalar;
49
+ in?: AggregateScalar[];
50
+ not?: AggregateScalar;
51
+ notIn?: AggregateScalar[];
52
+ contains?: string;
53
+ startsWith?: string;
54
+ gte?: number;
55
+ gt?: number;
56
+ lte?: number;
57
+ lt?: number;
58
+ /** true: the field has a value; false: it is empty. */
59
+ exists?: boolean;
60
+ };
61
+ export type AggregateMetric<N extends string, F extends string> = "count" | `uniq:${F}` | `${"sum" | "avg" | "min" | "max" | "p50" | "p90" | "p95" | "p99"}:${N}`;
62
+ type AggregateShape<D extends string, F extends string, N extends string> = {
63
+ dataset: D;
64
+ /** Inclusive start and exclusive end; at most 92 days apart (usage datasets: any range). */
65
+ from: string | Date;
66
+ to: string | Date;
67
+ where?: Partial<Record<F | N, AggregateFilter>>;
68
+ /** At most four. Numeric fields other than httpStatus are aggregated, not grouped. */
69
+ groupBy?: (F | AggregateTimeGroup)[];
70
+ /** One to eight; default ["count"]. */
71
+ metrics?: AggregateMetric<N, F>[];
72
+ orderBy?: {
73
+ by: string;
74
+ descending?: boolean;
75
+ };
76
+ /** Default 100, at most 1,000 rows (usage datasets: 10,000). */
77
+ limit?: number;
78
+ };
79
+ /** A structured aggregate over telemetry facts or permanent usage. The key's workspace/user scope always applies; logs and usage need an unscoped key. */
80
+ export type AggregateQuery = AggregateShape<"attempts", AttemptField, AttemptNumber> | AggregateShape<"requests", RequestField, RequestNumber> | AggregateShape<"logs", LogField, never> | AggregateShape<"usage", UsageField, UsageNumber> | AggregateShape<"usageAttempts", UsageAttemptField, UsageAttemptNumber>;
81
+ export type AggregateResult = {
82
+ /** Groupings first, then metrics, in request order. */
83
+ columns: string[];
84
+ rows: Record<string, Json>[];
85
+ /** More groups matched than `limit`. */
86
+ truncated: boolean;
87
+ };
31
88
  export type TelemetryReadOptions = TelemetryQuery & {
32
89
  signal?: AbortSignal;
33
90
  };
@@ -191,6 +248,8 @@ export declare class Constellation extends RequestClient {
191
248
  after?: number;
192
249
  }, signal?: AbortSignal) => Promise<Json>;
193
250
  };
251
+ /** Count, group and aggregate attempts, requests or logs, e.g. failed attempts per offering and error code. */
252
+ query: (query: AggregateQuery, signal?: AbortSignal) => Promise<AggregateResult>;
194
253
  logs: {
195
254
  searchSystem: (query?: LogSearchQuery & TelemetryQuery, signal?: AbortSignal) => Promise<Page<Json>>;
196
255
  searchRequests: (query?: Omit<LogSearchQuery, "level" | "levelNot" | "target" | "targetNot"> & {
@@ -204,5 +263,5 @@ export declare class Constellation extends RequestClient {
204
263
  verify: (request: Request, expectedToken: string) => Promise<WebhookEvent>;
205
264
  };
206
265
  }
207
- export { PermanentEventError, HookConfigurationError, EventReceiver, prepareResponse, processEvent, ClientTelemetry, PERMANENT_RESPONSE_CODES } from "../../client-core/src/agent";
208
- export type { EventStore, AgentHooks, HookContext, ToolResult } from "../../client-core/src/agent";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "TypeScript client for durable model requests and model events",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",