@dexto/server 1.6.16 → 1.6.17

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.
Files changed (37) hide show
  1. package/dist/events/a2a-sse-subscriber.cjs +9 -2
  2. package/dist/events/a2a-sse-subscriber.d.ts.map +1 -1
  3. package/dist/events/a2a-sse-subscriber.js +9 -2
  4. package/dist/events/usage-event-subscriber.cjs +263 -0
  5. package/dist/events/usage-event-subscriber.d.ts +35 -0
  6. package/dist/events/usage-event-subscriber.d.ts.map +1 -0
  7. package/dist/events/usage-event-subscriber.js +244 -0
  8. package/dist/events/usage-event-types.cjs +16 -0
  9. package/dist/events/usage-event-types.d.ts +33 -0
  10. package/dist/events/usage-event-types.d.ts.map +1 -0
  11. package/dist/events/usage-event-types.js +0 -0
  12. package/dist/hono/index.d.ts +157 -32
  13. package/dist/hono/index.d.ts.map +1 -1
  14. package/dist/hono/routes/a2a-tasks.d.ts +9 -9
  15. package/dist/hono/routes/approvals.cjs +94 -6
  16. package/dist/hono/routes/approvals.d.ts +22 -6
  17. package/dist/hono/routes/approvals.d.ts.map +1 -1
  18. package/dist/hono/routes/approvals.js +94 -6
  19. package/dist/hono/routes/messages.cjs +16 -5
  20. package/dist/hono/routes/messages.d.ts +6 -0
  21. package/dist/hono/routes/messages.d.ts.map +1 -1
  22. package/dist/hono/routes/messages.js +17 -6
  23. package/dist/hono/routes/search.d.ts +10 -0
  24. package/dist/hono/routes/search.d.ts.map +1 -1
  25. package/dist/hono/routes/sessions.cjs +54 -1
  26. package/dist/hono/routes/sessions.d.ts +111 -18
  27. package/dist/hono/routes/sessions.d.ts.map +1 -1
  28. package/dist/hono/routes/sessions.js +59 -2
  29. package/dist/hono/schemas/responses.cjs +48 -8
  30. package/dist/hono/schemas/responses.d.ts +489 -22
  31. package/dist/hono/schemas/responses.d.ts.map +1 -1
  32. package/dist/hono/schemas/responses.js +49 -9
  33. package/dist/index.cjs +4 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/package.json +9 -9
@@ -98,11 +98,18 @@ class A2ASseEventSubscriber {
98
98
  message: {
99
99
  role: "agent",
100
100
  content: [{ type: "text", text: payload.content }],
101
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
101
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
102
+ ...payload.messageId && { messageId: payload.messageId }
102
103
  },
103
104
  tokenUsage: payload.tokenUsage,
104
105
  provider: payload.provider,
105
- model: payload.model
106
+ model: payload.model,
107
+ ...payload.messageId && { messageId: payload.messageId },
108
+ ...payload.usageScopeId && { usageScopeId: payload.usageScopeId },
109
+ ...payload.estimatedCost !== void 0 && {
110
+ estimatedCost: payload.estimatedCost
111
+ },
112
+ ...payload.pricingStatus && { pricingStatus: payload.pricingStatus }
106
113
  });
107
114
  },
108
115
  { signal }
@@ -1 +1 @@
1
- {"version":3,"file":"a2a-sse-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/a2a-sse-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,qBAAqB,CAAC,CAAkB;IAEhD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAkHxC;;;;;;;OAOG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC;IAiDxD;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAKtB;;OAEG;IACH,OAAO,IAAI,IAAI;IAgBf;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;OAEG;IACH,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CASjD"}
1
+ {"version":3,"file":"a2a-sse-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/a2a-sse-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiB5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,qBAAqB,CAAC,CAAkB;IAEhD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAyHxC;;;;;;;OAOG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC;IAiDxD;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAKtB;;OAEG;IACH,OAAO,IAAI,IAAI;IAgBf;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;OAEG;IACH,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;CASjD"}
@@ -75,11 +75,18 @@ class A2ASseEventSubscriber {
75
75
  message: {
76
76
  role: "agent",
77
77
  content: [{ type: "text", text: payload.content }],
78
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
78
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
79
+ ...payload.messageId && { messageId: payload.messageId }
79
80
  },
80
81
  tokenUsage: payload.tokenUsage,
81
82
  provider: payload.provider,
82
- model: payload.model
83
+ model: payload.model,
84
+ ...payload.messageId && { messageId: payload.messageId },
85
+ ...payload.usageScopeId && { usageScopeId: payload.usageScopeId },
86
+ ...payload.estimatedCost !== void 0 && {
87
+ estimatedCost: payload.estimatedCost
88
+ },
89
+ ...payload.pricingStatus && { pricingStatus: payload.pricingStatus }
83
90
  });
84
91
  },
85
92
  { signal }
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var usage_event_subscriber_exports = {};
20
+ __export(usage_event_subscriber_exports, {
21
+ UsageEventSubscriber: () => UsageEventSubscriber
22
+ });
23
+ module.exports = __toCommonJS(usage_event_subscriber_exports);
24
+ var import_events = require("events");
25
+ var import_core = require("@dexto/core");
26
+ const OUTBOX_KEY_PREFIX = "usage-outbox:";
27
+ const DEFAULT_DELIVERY_OPTIONS = {
28
+ fetchFn: fetch,
29
+ flushIntervalMs: 5e3,
30
+ batchSize: 50,
31
+ requestTimeoutMs: 1e4
32
+ };
33
+ function requirePositiveIntegerOption(name, value) {
34
+ if (!Number.isInteger(value) || value <= 0) {
35
+ throw new RangeError(
36
+ `UsageEventSubscriber ${name} must be a positive integer. Received: ${value}`
37
+ );
38
+ }
39
+ return value;
40
+ }
41
+ function createUsageEventId(usageScopeId, messageId) {
42
+ return `usage:${usageScopeId}:${messageId}`;
43
+ }
44
+ function createOutboxKey(eventId) {
45
+ return `${OUTBOX_KEY_PREFIX}${eventId}`;
46
+ }
47
+ class UsageEventSubscriber {
48
+ database;
49
+ targetUrl;
50
+ authToken;
51
+ runtimeId;
52
+ runId;
53
+ deliveryOptions;
54
+ abortController;
55
+ inFlightDeliveryAbortController;
56
+ flushInterval;
57
+ flushPromise = null;
58
+ flushRequestedWhileRunning = false;
59
+ isCleaningUp = false;
60
+ constructor(config) {
61
+ this.database = config.database;
62
+ this.targetUrl = config.targetUrl;
63
+ this.authToken = config.authToken;
64
+ this.runtimeId = config.runtimeId;
65
+ this.runId = config.runId;
66
+ const flushIntervalMs = requirePositiveIntegerOption(
67
+ "flushIntervalMs",
68
+ config.flushIntervalMs ?? DEFAULT_DELIVERY_OPTIONS.flushIntervalMs
69
+ );
70
+ const batchSize = requirePositiveIntegerOption(
71
+ "batchSize",
72
+ config.batchSize ?? DEFAULT_DELIVERY_OPTIONS.batchSize
73
+ );
74
+ const requestTimeoutMs = requirePositiveIntegerOption(
75
+ "requestTimeoutMs",
76
+ config.requestTimeoutMs ?? DEFAULT_DELIVERY_OPTIONS.requestTimeoutMs
77
+ );
78
+ this.deliveryOptions = {
79
+ fetchFn: config.fetchFn ?? DEFAULT_DELIVERY_OPTIONS.fetchFn,
80
+ flushIntervalMs,
81
+ batchSize,
82
+ requestTimeoutMs
83
+ };
84
+ }
85
+ subscribe(eventBus) {
86
+ this.isCleaningUp = false;
87
+ this.abortController?.abort();
88
+ if (this.flushInterval) {
89
+ clearInterval(this.flushInterval);
90
+ }
91
+ this.abortController = new AbortController();
92
+ const { signal } = this.abortController;
93
+ (0, import_events.setMaxListeners)(10, signal);
94
+ eventBus.on(
95
+ "llm:response",
96
+ (payload) => {
97
+ void this.enqueueUsageEvent(payload);
98
+ },
99
+ { signal }
100
+ );
101
+ this.flushInterval = setInterval(() => {
102
+ void this.flushPending();
103
+ }, this.deliveryOptions.flushIntervalMs);
104
+ void this.flushPending();
105
+ }
106
+ cleanup() {
107
+ this.isCleaningUp = true;
108
+ if (this.abortController) {
109
+ this.abortController.abort();
110
+ delete this.abortController;
111
+ }
112
+ if (this.flushInterval) {
113
+ clearInterval(this.flushInterval);
114
+ delete this.flushInterval;
115
+ }
116
+ if (this.inFlightDeliveryAbortController) {
117
+ this.inFlightDeliveryAbortController.abort();
118
+ delete this.inFlightDeliveryAbortController;
119
+ }
120
+ }
121
+ async flush() {
122
+ await this.flushPending();
123
+ }
124
+ buildUsageEvent(payload) {
125
+ if (!payload.messageId || !payload.usageScopeId || !payload.tokenUsage) {
126
+ return null;
127
+ }
128
+ if (!(0, import_core.hasMeaningfulTokenUsage)(payload.tokenUsage)) {
129
+ return null;
130
+ }
131
+ const resolvedCostBreakdown = payload.provider && payload.model ? (() => {
132
+ const pricing = (0, import_core.getModelPricing)(payload.provider, payload.model);
133
+ if (!pricing) {
134
+ return void 0;
135
+ }
136
+ return (0, import_core.calculateCostBreakdown)(payload.tokenUsage, pricing);
137
+ })() : void 0;
138
+ const resolvedEstimatedCost = payload.estimatedCost ?? resolvedCostBreakdown?.totalUsd;
139
+ return {
140
+ eventId: createUsageEventId(payload.usageScopeId, payload.messageId),
141
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
142
+ sessionId: payload.sessionId,
143
+ messageId: payload.messageId,
144
+ usageScopeId: payload.usageScopeId,
145
+ ...payload.provider && { provider: payload.provider },
146
+ ...payload.model && { model: payload.model },
147
+ tokenUsage: payload.tokenUsage,
148
+ ...resolvedEstimatedCost !== void 0 && { estimatedCostUsd: resolvedEstimatedCost },
149
+ ...resolvedCostBreakdown && { costBreakdownUsd: resolvedCostBreakdown },
150
+ ...this.runtimeId && { runtimeId: this.runtimeId },
151
+ ...this.runId && { runId: this.runId }
152
+ };
153
+ }
154
+ async enqueueUsageEvent(payload) {
155
+ const usageEvent = this.buildUsageEvent(payload);
156
+ if (!usageEvent) {
157
+ return;
158
+ }
159
+ await this.database.set(createOutboxKey(usageEvent.eventId), {
160
+ event: usageEvent
161
+ });
162
+ void this.flushPending();
163
+ }
164
+ async listPendingRecords() {
165
+ const keys = await this.database.list(OUTBOX_KEY_PREFIX);
166
+ if (keys.length === 0) {
167
+ return [];
168
+ }
169
+ const records = await Promise.all(
170
+ keys.map(async (key) => {
171
+ const record = await this.database.get(key);
172
+ return record ? { key, record } : null;
173
+ })
174
+ );
175
+ return records.filter(
176
+ (value) => value !== null
177
+ ).sort(
178
+ (left, right) => left.record.event.occurredAt.localeCompare(right.record.event.occurredAt)
179
+ );
180
+ }
181
+ async flushPending() {
182
+ if (this.isCleaningUp) {
183
+ return this.flushPromise ?? Promise.resolve();
184
+ }
185
+ if (this.flushPromise) {
186
+ this.flushRequestedWhileRunning = true;
187
+ return this.flushPromise;
188
+ }
189
+ this.flushPromise = this.doFlushPending().finally(() => {
190
+ this.flushPromise = null;
191
+ const shouldFlushAgain = this.flushRequestedWhileRunning;
192
+ this.flushRequestedWhileRunning = false;
193
+ if (shouldFlushAgain) {
194
+ void this.flushPending();
195
+ }
196
+ });
197
+ return this.flushPromise;
198
+ }
199
+ async doFlushPending() {
200
+ if (this.isCleaningUp) {
201
+ return;
202
+ }
203
+ const pendingRecords = await this.listPendingRecords();
204
+ if (pendingRecords.length === 0) {
205
+ return;
206
+ }
207
+ for (let index = 0; index < pendingRecords.length; index += this.deliveryOptions.batchSize) {
208
+ if (this.isCleaningUp) {
209
+ return;
210
+ }
211
+ const batchRecords = pendingRecords.slice(
212
+ index,
213
+ index + this.deliveryOptions.batchSize
214
+ );
215
+ const payload = {
216
+ events: batchRecords.map((record) => record.record.event)
217
+ };
218
+ const abortController = new AbortController();
219
+ this.inFlightDeliveryAbortController = abortController;
220
+ const timeout = setTimeout(() => {
221
+ abortController.abort();
222
+ }, this.deliveryOptions.requestTimeoutMs);
223
+ try {
224
+ const response = await this.deliveryOptions.fetchFn(this.targetUrl, {
225
+ method: "POST",
226
+ headers: {
227
+ Authorization: `Bearer ${this.authToken}`,
228
+ "Content-Type": "application/json"
229
+ },
230
+ body: JSON.stringify(payload),
231
+ signal: abortController.signal
232
+ });
233
+ if (!response.ok) {
234
+ import_core.logger.warn(
235
+ `Usage event delivery failed (${response.status}) for ${batchRecords.length} events`
236
+ );
237
+ return;
238
+ }
239
+ if (this.isCleaningUp) {
240
+ return;
241
+ }
242
+ await Promise.all(batchRecords.map((record) => this.database.delete(record.key)));
243
+ } catch (error) {
244
+ if (this.isCleaningUp && abortController.signal.aborted) {
245
+ return;
246
+ }
247
+ import_core.logger.warn(
248
+ `Usage event delivery failed: ${error instanceof Error ? error.message : String(error)}`
249
+ );
250
+ return;
251
+ } finally {
252
+ clearTimeout(timeout);
253
+ if (this.inFlightDeliveryAbortController === abortController) {
254
+ delete this.inFlightDeliveryAbortController;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ // Annotate the CommonJS export names for ESM import in node:
261
+ 0 && (module.exports = {
262
+ UsageEventSubscriber
263
+ });
@@ -0,0 +1,35 @@
1
+ import { AgentEventBus, type Database } from '@dexto/core';
2
+ import type { EventSubscriber } from './types.js';
3
+ import type { UsageEventDeliveryOptions } from './usage-event-types.js';
4
+ interface UsageEventSubscriberConfig extends UsageEventDeliveryOptions {
5
+ database: Database;
6
+ targetUrl: string;
7
+ authToken: string;
8
+ runtimeId?: string;
9
+ runId?: string;
10
+ }
11
+ export declare class UsageEventSubscriber implements EventSubscriber {
12
+ private readonly database;
13
+ private readonly targetUrl;
14
+ private readonly authToken;
15
+ private readonly runtimeId;
16
+ private readonly runId;
17
+ private readonly deliveryOptions;
18
+ private abortController?;
19
+ private inFlightDeliveryAbortController?;
20
+ private flushInterval?;
21
+ private flushPromise;
22
+ private flushRequestedWhileRunning;
23
+ private isCleaningUp;
24
+ constructor(config: UsageEventSubscriberConfig);
25
+ subscribe(eventBus: AgentEventBus): void;
26
+ cleanup(): void;
27
+ flush(): Promise<void>;
28
+ private buildUsageEvent;
29
+ private enqueueUsageEvent;
30
+ private listPendingRecords;
31
+ private flushPending;
32
+ private doFlushPending;
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=usage-event-subscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-event-subscriber.d.ts","sourceRoot":"","sources":["../../src/events/usage-event-subscriber.ts"],"names":[],"mappings":"AACA,OAAO,EACH,aAAa,EAMb,KAAK,QAAQ,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGR,yBAAyB,EAC5B,MAAM,wBAAwB,CAAC;AAqBhC,UAAU,0BAA2B,SAAQ,yBAAyB;IAClE,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,qBAAa,oBAAqB,YAAW,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IACtE,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,+BAA+B,CAAC,CAAkB;IAC1D,OAAO,CAAC,aAAa,CAAC,CAAiC;IACvD,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,0BAA0B,CAAS;IAC3C,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,0BAA0B;IA0B9C,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IA2BxC,OAAO,IAAI,IAAI;IAmBF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC,OAAO,CAAC,eAAe;YAsCT,iBAAiB;YAajB,kBAAkB;YA2BlB,YAAY;YAsBZ,cAAc;CA2E/B"}
@@ -0,0 +1,244 @@
1
+ import { setMaxListeners } from "events";
2
+ import {
3
+ calculateCostBreakdown,
4
+ getModelPricing,
5
+ hasMeaningfulTokenUsage,
6
+ logger
7
+ } from "@dexto/core";
8
+ const OUTBOX_KEY_PREFIX = "usage-outbox:";
9
+ const DEFAULT_DELIVERY_OPTIONS = {
10
+ fetchFn: fetch,
11
+ flushIntervalMs: 5e3,
12
+ batchSize: 50,
13
+ requestTimeoutMs: 1e4
14
+ };
15
+ function requirePositiveIntegerOption(name, value) {
16
+ if (!Number.isInteger(value) || value <= 0) {
17
+ throw new RangeError(
18
+ `UsageEventSubscriber ${name} must be a positive integer. Received: ${value}`
19
+ );
20
+ }
21
+ return value;
22
+ }
23
+ function createUsageEventId(usageScopeId, messageId) {
24
+ return `usage:${usageScopeId}:${messageId}`;
25
+ }
26
+ function createOutboxKey(eventId) {
27
+ return `${OUTBOX_KEY_PREFIX}${eventId}`;
28
+ }
29
+ class UsageEventSubscriber {
30
+ database;
31
+ targetUrl;
32
+ authToken;
33
+ runtimeId;
34
+ runId;
35
+ deliveryOptions;
36
+ abortController;
37
+ inFlightDeliveryAbortController;
38
+ flushInterval;
39
+ flushPromise = null;
40
+ flushRequestedWhileRunning = false;
41
+ isCleaningUp = false;
42
+ constructor(config) {
43
+ this.database = config.database;
44
+ this.targetUrl = config.targetUrl;
45
+ this.authToken = config.authToken;
46
+ this.runtimeId = config.runtimeId;
47
+ this.runId = config.runId;
48
+ const flushIntervalMs = requirePositiveIntegerOption(
49
+ "flushIntervalMs",
50
+ config.flushIntervalMs ?? DEFAULT_DELIVERY_OPTIONS.flushIntervalMs
51
+ );
52
+ const batchSize = requirePositiveIntegerOption(
53
+ "batchSize",
54
+ config.batchSize ?? DEFAULT_DELIVERY_OPTIONS.batchSize
55
+ );
56
+ const requestTimeoutMs = requirePositiveIntegerOption(
57
+ "requestTimeoutMs",
58
+ config.requestTimeoutMs ?? DEFAULT_DELIVERY_OPTIONS.requestTimeoutMs
59
+ );
60
+ this.deliveryOptions = {
61
+ fetchFn: config.fetchFn ?? DEFAULT_DELIVERY_OPTIONS.fetchFn,
62
+ flushIntervalMs,
63
+ batchSize,
64
+ requestTimeoutMs
65
+ };
66
+ }
67
+ subscribe(eventBus) {
68
+ this.isCleaningUp = false;
69
+ this.abortController?.abort();
70
+ if (this.flushInterval) {
71
+ clearInterval(this.flushInterval);
72
+ }
73
+ this.abortController = new AbortController();
74
+ const { signal } = this.abortController;
75
+ setMaxListeners(10, signal);
76
+ eventBus.on(
77
+ "llm:response",
78
+ (payload) => {
79
+ void this.enqueueUsageEvent(payload);
80
+ },
81
+ { signal }
82
+ );
83
+ this.flushInterval = setInterval(() => {
84
+ void this.flushPending();
85
+ }, this.deliveryOptions.flushIntervalMs);
86
+ void this.flushPending();
87
+ }
88
+ cleanup() {
89
+ this.isCleaningUp = true;
90
+ if (this.abortController) {
91
+ this.abortController.abort();
92
+ delete this.abortController;
93
+ }
94
+ if (this.flushInterval) {
95
+ clearInterval(this.flushInterval);
96
+ delete this.flushInterval;
97
+ }
98
+ if (this.inFlightDeliveryAbortController) {
99
+ this.inFlightDeliveryAbortController.abort();
100
+ delete this.inFlightDeliveryAbortController;
101
+ }
102
+ }
103
+ async flush() {
104
+ await this.flushPending();
105
+ }
106
+ buildUsageEvent(payload) {
107
+ if (!payload.messageId || !payload.usageScopeId || !payload.tokenUsage) {
108
+ return null;
109
+ }
110
+ if (!hasMeaningfulTokenUsage(payload.tokenUsage)) {
111
+ return null;
112
+ }
113
+ const resolvedCostBreakdown = payload.provider && payload.model ? (() => {
114
+ const pricing = getModelPricing(payload.provider, payload.model);
115
+ if (!pricing) {
116
+ return void 0;
117
+ }
118
+ return calculateCostBreakdown(payload.tokenUsage, pricing);
119
+ })() : void 0;
120
+ const resolvedEstimatedCost = payload.estimatedCost ?? resolvedCostBreakdown?.totalUsd;
121
+ return {
122
+ eventId: createUsageEventId(payload.usageScopeId, payload.messageId),
123
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
124
+ sessionId: payload.sessionId,
125
+ messageId: payload.messageId,
126
+ usageScopeId: payload.usageScopeId,
127
+ ...payload.provider && { provider: payload.provider },
128
+ ...payload.model && { model: payload.model },
129
+ tokenUsage: payload.tokenUsage,
130
+ ...resolvedEstimatedCost !== void 0 && { estimatedCostUsd: resolvedEstimatedCost },
131
+ ...resolvedCostBreakdown && { costBreakdownUsd: resolvedCostBreakdown },
132
+ ...this.runtimeId && { runtimeId: this.runtimeId },
133
+ ...this.runId && { runId: this.runId }
134
+ };
135
+ }
136
+ async enqueueUsageEvent(payload) {
137
+ const usageEvent = this.buildUsageEvent(payload);
138
+ if (!usageEvent) {
139
+ return;
140
+ }
141
+ await this.database.set(createOutboxKey(usageEvent.eventId), {
142
+ event: usageEvent
143
+ });
144
+ void this.flushPending();
145
+ }
146
+ async listPendingRecords() {
147
+ const keys = await this.database.list(OUTBOX_KEY_PREFIX);
148
+ if (keys.length === 0) {
149
+ return [];
150
+ }
151
+ const records = await Promise.all(
152
+ keys.map(async (key) => {
153
+ const record = await this.database.get(key);
154
+ return record ? { key, record } : null;
155
+ })
156
+ );
157
+ return records.filter(
158
+ (value) => value !== null
159
+ ).sort(
160
+ (left, right) => left.record.event.occurredAt.localeCompare(right.record.event.occurredAt)
161
+ );
162
+ }
163
+ async flushPending() {
164
+ if (this.isCleaningUp) {
165
+ return this.flushPromise ?? Promise.resolve();
166
+ }
167
+ if (this.flushPromise) {
168
+ this.flushRequestedWhileRunning = true;
169
+ return this.flushPromise;
170
+ }
171
+ this.flushPromise = this.doFlushPending().finally(() => {
172
+ this.flushPromise = null;
173
+ const shouldFlushAgain = this.flushRequestedWhileRunning;
174
+ this.flushRequestedWhileRunning = false;
175
+ if (shouldFlushAgain) {
176
+ void this.flushPending();
177
+ }
178
+ });
179
+ return this.flushPromise;
180
+ }
181
+ async doFlushPending() {
182
+ if (this.isCleaningUp) {
183
+ return;
184
+ }
185
+ const pendingRecords = await this.listPendingRecords();
186
+ if (pendingRecords.length === 0) {
187
+ return;
188
+ }
189
+ for (let index = 0; index < pendingRecords.length; index += this.deliveryOptions.batchSize) {
190
+ if (this.isCleaningUp) {
191
+ return;
192
+ }
193
+ const batchRecords = pendingRecords.slice(
194
+ index,
195
+ index + this.deliveryOptions.batchSize
196
+ );
197
+ const payload = {
198
+ events: batchRecords.map((record) => record.record.event)
199
+ };
200
+ const abortController = new AbortController();
201
+ this.inFlightDeliveryAbortController = abortController;
202
+ const timeout = setTimeout(() => {
203
+ abortController.abort();
204
+ }, this.deliveryOptions.requestTimeoutMs);
205
+ try {
206
+ const response = await this.deliveryOptions.fetchFn(this.targetUrl, {
207
+ method: "POST",
208
+ headers: {
209
+ Authorization: `Bearer ${this.authToken}`,
210
+ "Content-Type": "application/json"
211
+ },
212
+ body: JSON.stringify(payload),
213
+ signal: abortController.signal
214
+ });
215
+ if (!response.ok) {
216
+ logger.warn(
217
+ `Usage event delivery failed (${response.status}) for ${batchRecords.length} events`
218
+ );
219
+ return;
220
+ }
221
+ if (this.isCleaningUp) {
222
+ return;
223
+ }
224
+ await Promise.all(batchRecords.map((record) => this.database.delete(record.key)));
225
+ } catch (error) {
226
+ if (this.isCleaningUp && abortController.signal.aborted) {
227
+ return;
228
+ }
229
+ logger.warn(
230
+ `Usage event delivery failed: ${error instanceof Error ? error.message : String(error)}`
231
+ );
232
+ return;
233
+ } finally {
234
+ clearTimeout(timeout);
235
+ if (this.inFlightDeliveryAbortController === abortController) {
236
+ delete this.inFlightDeliveryAbortController;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ export {
243
+ UsageEventSubscriber
244
+ };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+ var usage_event_types_exports = {};
16
+ module.exports = __toCommonJS(usage_event_types_exports);
@@ -0,0 +1,33 @@
1
+ import type { LLMProvider, TokenUsage } from '@dexto/core';
2
+ export interface UsageEventCostBreakdown {
3
+ inputUsd: number;
4
+ outputUsd: number;
5
+ reasoningUsd: number;
6
+ cacheReadUsd: number;
7
+ cacheWriteUsd: number;
8
+ totalUsd: number;
9
+ }
10
+ export interface UsageEvent {
11
+ eventId: string;
12
+ occurredAt: string;
13
+ sessionId: string;
14
+ messageId: string;
15
+ usageScopeId: string;
16
+ provider?: LLMProvider;
17
+ model?: string;
18
+ tokenUsage: TokenUsage;
19
+ estimatedCostUsd?: number;
20
+ costBreakdownUsd?: UsageEventCostBreakdown;
21
+ runtimeId?: string;
22
+ runId?: string;
23
+ }
24
+ export interface UsageEventBatch {
25
+ events: UsageEvent[];
26
+ }
27
+ export interface UsageEventDeliveryOptions {
28
+ fetchFn?: typeof globalThis.fetch;
29
+ flushIntervalMs?: number;
30
+ batchSize?: number;
31
+ requestTimeoutMs?: number;
32
+ }
33
+ //# sourceMappingURL=usage-event-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-event-types.d.ts","sourceRoot":"","sources":["../../src/events/usage-event-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,uBAAuB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACtC,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B"}
File without changes