@eidentic/server 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1590 @@
1
+ // src/index.ts
2
+ import { Hono } from "hono";
3
+ import { streamSSE } from "hono/streaming";
4
+ import { bodyLimit } from "hono/body-limit";
5
+ import { cors } from "hono/cors";
6
+ import { createWorkflowRunRegistry } from "@eidentic/workflow";
7
+
8
+ // src/rate-limit.ts
9
+ var InMemoryTokenBucketLimiter = class {
10
+ capacity;
11
+ refillPerSec;
12
+ now;
13
+ buckets = /* @__PURE__ */ new Map();
14
+ /**
15
+ * The eviction threshold in ms: entries older than this are guaranteed to be
16
+ * at full capacity, so removing them is lossless.
17
+ * = (capacity / refillPerSec) * 1000 * 2
18
+ * For zero-refill configs a fixed 24-hour window bounds growth.
19
+ */
20
+ evictThresholdMs;
21
+ /**
22
+ * Sweep runs at most once per this interval. Set to half the eviction
23
+ * threshold so that stale entries are caught within at most one extra window.
24
+ */
25
+ sweepIntervalMs;
26
+ lastSweepMs = 0;
27
+ constructor(opts) {
28
+ this.capacity = opts.capacity;
29
+ this.refillPerSec = opts.refillPerSec;
30
+ this.now = opts.now ?? (() => Date.now());
31
+ if (opts.refillPerSec > 0) {
32
+ const fullRefillMs = opts.capacity / opts.refillPerSec * 1e3;
33
+ this.evictThresholdMs = fullRefillMs * 2;
34
+ this.sweepIntervalMs = fullRefillMs;
35
+ } else {
36
+ this.evictThresholdMs = 864e5;
37
+ this.sweepIntervalMs = 36e5;
38
+ }
39
+ }
40
+ /**
41
+ * Test-only accessor: number of entries currently held in the buckets map.
42
+ * Not part of the `RateLimiterPort` contract.
43
+ */
44
+ get bucketCount() {
45
+ return this.buckets.size;
46
+ }
47
+ acquire(key, cost = 1) {
48
+ const nowMs = this.now();
49
+ if (nowMs - this.lastSweepMs >= this.sweepIntervalMs) {
50
+ this._sweep(nowMs);
51
+ this.lastSweepMs = nowMs;
52
+ }
53
+ let bucket = this.buckets.get(key);
54
+ if (!bucket) {
55
+ bucket = { tokens: this.capacity, lastRefillMs: nowMs };
56
+ this.buckets.set(key, bucket);
57
+ }
58
+ const elapsedSec = (nowMs - bucket.lastRefillMs) / 1e3;
59
+ bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsedSec * this.refillPerSec);
60
+ bucket.lastRefillMs = nowMs;
61
+ if (bucket.tokens >= cost) {
62
+ bucket.tokens -= cost;
63
+ return { ok: true, remaining: Math.floor(bucket.tokens) };
64
+ }
65
+ const retryAfterMs = this.refillPerSec > 0 ? Math.ceil((cost - bucket.tokens) / this.refillPerSec * 1e3) : void 0;
66
+ return { ok: false, retryAfterMs, remaining: Math.floor(bucket.tokens) };
67
+ }
68
+ /**
69
+ * Evict bucket entries older than `evictThresholdMs`. Called opportunistically
70
+ * from `acquire` — no timer involved.
71
+ */
72
+ _sweep(nowMs) {
73
+ for (const [k, state] of this.buckets) {
74
+ if (nowMs - state.lastRefillMs > this.evictThresholdMs) {
75
+ this.buckets.delete(k);
76
+ }
77
+ }
78
+ }
79
+ };
80
+
81
+ // src/quota.ts
82
+ var InMemoryQuota = class {
83
+ resolve;
84
+ ledger = /* @__PURE__ */ new Map();
85
+ /** In-flight run count per key: incremented on check, decremented on record/release. */
86
+ reserved = /* @__PURE__ */ new Map();
87
+ /** Per-key monotonic generation counter, bumped on reset() to invalidate outstanding tokens. */
88
+ generations = /* @__PURE__ */ new Map();
89
+ /** All live reservation tokens, used by the max-age sweep. */
90
+ activeReservations = /* @__PURE__ */ new Set();
91
+ /** Max-age sweep interval handle — cleared on destroy(). */
92
+ sweepInterval;
93
+ /** How old a reservation must be (ms) before the sweep discards it. Default 5 minutes. */
94
+ reservationMaxAgeMs;
95
+ constructor(limits, options) {
96
+ this.resolve = typeof limits === "function" ? limits : () => limits;
97
+ this.reservationMaxAgeMs = options?.reservationMaxAgeMs ?? 3e5;
98
+ if (this.reservationMaxAgeMs > 0 && isFinite(this.reservationMaxAgeMs)) {
99
+ this.sweepInterval = setInterval(() => {
100
+ this.sweepStaleReservations();
101
+ }, this.reservationMaxAgeMs);
102
+ if (typeof this.sweepInterval === "object" && this.sweepInterval !== null && typeof this.sweepInterval.unref === "function") {
103
+ this.sweepInterval.unref();
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Release all stale reservations (older than `reservationMaxAgeMs`).
109
+ * Called automatically by the background sweep, but also available for testing.
110
+ */
111
+ sweepStaleReservations() {
112
+ const now = Date.now();
113
+ for (const reservation of this.activeReservations) {
114
+ if (now - reservation.createdAt >= this.reservationMaxAgeMs) {
115
+ this.activeReservations.delete(reservation);
116
+ if (reservation.generation === this.getGeneration(reservation.key)) {
117
+ const cur = this.getReserved(reservation.key);
118
+ if (cur > 0) this.reserved.set(reservation.key, cur - 1);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Stop the background sweep interval. Call this when shutting down to prevent
125
+ * open handle warnings in tests and to release resources cleanly.
126
+ */
127
+ destroy() {
128
+ if (this.sweepInterval !== void 0) {
129
+ clearInterval(this.sweepInterval);
130
+ }
131
+ }
132
+ getUsage(key) {
133
+ let u = this.ledger.get(key);
134
+ if (!u) {
135
+ u = { usd: 0, tokens: 0, runs: 0 };
136
+ this.ledger.set(key, u);
137
+ }
138
+ return u;
139
+ }
140
+ getGeneration(key) {
141
+ return this.generations.get(key) ?? 0;
142
+ }
143
+ getReserved(key) {
144
+ return this.reserved.get(key) ?? 0;
145
+ }
146
+ /**
147
+ * Check whether `key` may start another run. On success, reserves 1 run in the in-flight
148
+ * counter and returns a `reservation` token. Hard ceilings are checked against
149
+ * `committed + reserved` so concurrent callers see each other's pending runs.
150
+ *
151
+ * Always call `record(key, spend, reservation)` or `release(reservation)` after `check`
152
+ * to free the reservation and prevent in-flight count leakage.
153
+ */
154
+ check(key) {
155
+ const limits = this.resolve(key);
156
+ const usage = this.getUsage(key);
157
+ const inFlight = this.getReserved(key);
158
+ if (limits.hardUsd !== void 0 && usage.usd >= limits.hardUsd) {
159
+ return { ok: false, reason: `hard USD ceiling reached (${usage.usd} >= ${limits.hardUsd})`, usage };
160
+ }
161
+ if (limits.hardTokens !== void 0 && usage.tokens >= limits.hardTokens) {
162
+ return { ok: false, reason: `hard token ceiling reached (${usage.tokens} >= ${limits.hardTokens})`, usage };
163
+ }
164
+ if (limits.hardRuns !== void 0 && usage.runs + inFlight >= limits.hardRuns) {
165
+ return { ok: false, reason: `hard run ceiling reached (${usage.runs + inFlight} >= ${limits.hardRuns})`, usage };
166
+ }
167
+ this.reserved.set(key, inFlight + 1);
168
+ const generation = this.getGeneration(key);
169
+ const reservation = { key, generation, createdAt: Date.now() };
170
+ this.activeReservations.add(reservation);
171
+ const warn = limits.softUsd !== void 0 && usage.usd >= limits.softUsd;
172
+ return { ok: true, warn: warn || void 0, usage, reservation };
173
+ }
174
+ /**
175
+ * Settle a reservation by recording actual spend. The 1-run reservation is consumed and the
176
+ * committed counter is updated with the real spend. If `reservation` is provided and stale
177
+ * (reset() was called between check and record), the settle is a no-op — no double-counting.
178
+ *
179
+ * Legacy callers that omit `reservation` still have their spend committed (backward-compatible),
180
+ * but the in-flight counter is not decremented (they weren't reserving).
181
+ */
182
+ record(key, spend, reservation) {
183
+ if (reservation !== void 0) {
184
+ this.activeReservations.delete(reservation);
185
+ if (reservation.generation === this.getGeneration(key)) {
186
+ const cur = this.getReserved(key);
187
+ if (cur > 0) this.reserved.set(key, cur - 1);
188
+ }
189
+ }
190
+ const usage = this.getUsage(key);
191
+ usage.usd += spend.usd;
192
+ usage.tokens += spend.tokens;
193
+ usage.runs += 1;
194
+ }
195
+ /**
196
+ * Release a reservation WITHOUT recording spend (error / abort path).
197
+ * If the token is stale (reset() was called), this is a no-op.
198
+ */
199
+ release(reservation) {
200
+ this.activeReservations.delete(reservation);
201
+ if (reservation.generation !== this.getGeneration(reservation.key)) return;
202
+ const cur = this.getReserved(reservation.key);
203
+ if (cur > 0) this.reserved.set(reservation.key, cur - 1);
204
+ }
205
+ /**
206
+ * Reset usage counters for a specific key, or ALL keys when called with no argument.
207
+ * Increments the generation counter for affected keys so outstanding reservation tokens
208
+ * become stale and cannot settle against the fresh state.
209
+ * Useful for tests and dev tooling.
210
+ */
211
+ reset(key) {
212
+ if (key !== void 0) {
213
+ this.ledger.delete(key);
214
+ this.reserved.delete(key);
215
+ this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
216
+ for (const r of this.activeReservations) {
217
+ if (r.key === key) this.activeReservations.delete(r);
218
+ }
219
+ } else {
220
+ this.ledger.clear();
221
+ this.reserved.clear();
222
+ for (const k of [...this.generations.keys()]) {
223
+ this.generations.set(k, (this.generations.get(k) ?? 0) + 1);
224
+ }
225
+ this.activeReservations.clear();
226
+ }
227
+ }
228
+ };
229
+
230
+ // src/ui-message-stream.ts
231
+ import {
232
+ createUIMessageStream,
233
+ createUIMessageStreamResponse
234
+ } from "ai";
235
+ function toUIMessageStream(events) {
236
+ return createUIMessageStream({
237
+ execute: async ({ writer }) => {
238
+ writer.write({ type: "start" });
239
+ writer.write({ type: "start-step" });
240
+ let textBlockOpen = false;
241
+ let textBlockId = "text-0";
242
+ function openTextBlock(id) {
243
+ if (!textBlockOpen) {
244
+ textBlockId = id;
245
+ writer.write({ type: "text-start", id: textBlockId });
246
+ textBlockOpen = true;
247
+ }
248
+ }
249
+ function closeTextBlock() {
250
+ if (textBlockOpen) {
251
+ writer.write({ type: "text-end", id: textBlockId });
252
+ textBlockOpen = false;
253
+ }
254
+ }
255
+ for await (const ev of events) {
256
+ switch (ev.type) {
257
+ // ------------------------------------------------------------------
258
+ // Streaming token delta — primary hot path for live text streaming
259
+ // ------------------------------------------------------------------
260
+ case "stream.delta": {
261
+ openTextBlock("streaming-text");
262
+ writer.write({ type: "text-delta", delta: ev.delta.text, id: textBlockId });
263
+ break;
264
+ }
265
+ // ------------------------------------------------------------------
266
+ // Assistant turn — complete content blocks (text + tool_use)
267
+ // ------------------------------------------------------------------
268
+ case "assistant": {
269
+ closeTextBlock();
270
+ for (const block of ev.content) {
271
+ if (block.type === "text") {
272
+ const id = `text-${crypto.randomUUID()}`;
273
+ writer.write({ type: "text-start", id });
274
+ writer.write({ type: "text-delta", delta: block.text, id });
275
+ writer.write({ type: "text-end", id });
276
+ } else if (block.type === "tool_use") {
277
+ writer.write({
278
+ type: "tool-input-available",
279
+ toolCallId: block.callId,
280
+ toolName: block.name,
281
+ input: block.input
282
+ });
283
+ }
284
+ }
285
+ break;
286
+ }
287
+ // ------------------------------------------------------------------
288
+ // Tool result
289
+ // ------------------------------------------------------------------
290
+ case "tool.result": {
291
+ if (ev.isError) {
292
+ writer.write({
293
+ type: "tool-output-error",
294
+ toolCallId: ev.callId,
295
+ errorText: typeof ev.output === "string" ? ev.output : JSON.stringify(ev.output)
296
+ });
297
+ } else {
298
+ writer.write({
299
+ type: "tool-output-available",
300
+ toolCallId: ev.callId,
301
+ output: ev.output
302
+ });
303
+ }
304
+ break;
305
+ }
306
+ // ------------------------------------------------------------------
307
+ // Terminal result — stream is done
308
+ // ------------------------------------------------------------------
309
+ case "result": {
310
+ closeTextBlock();
311
+ const finishReason = (() => {
312
+ switch (ev.subtype) {
313
+ case "success":
314
+ return "stop";
315
+ case "max_tokens":
316
+ return "length";
317
+ case "error":
318
+ return "error";
319
+ case "max_turns":
320
+ case "max_cost":
321
+ case "max_wall_clock":
322
+ case "aborted":
323
+ case "suspended":
324
+ default:
325
+ return "other";
326
+ }
327
+ })();
328
+ writer.write({ type: "finish-step" });
329
+ writer.write({ type: "finish", finishReason });
330
+ break;
331
+ }
332
+ // ------------------------------------------------------------------
333
+ // Ignored events — metadata / audit signals not meaningful to UI
334
+ // ------------------------------------------------------------------
335
+ case "session.init":
336
+ case "compaction":
337
+ break;
338
+ }
339
+ }
340
+ },
341
+ onError: (err) => {
342
+ const msg = err instanceof Error ? err.message : String(err);
343
+ return `Stream error: ${msg}`;
344
+ }
345
+ });
346
+ }
347
+ function toUIMessageStreamResponse(events, opts = {}) {
348
+ return createUIMessageStreamResponse({
349
+ stream: toUIMessageStream(events),
350
+ status: opts.status,
351
+ headers: opts.headers
352
+ });
353
+ }
354
+
355
+ // src/scheduler.ts
356
+ import { CronExpressionParser } from "cron-parser";
357
+ var realClock = {
358
+ now: () => Date.now()
359
+ };
360
+ var realTimer = {
361
+ setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
362
+ clearInterval: (handle) => globalThis.clearInterval(handle)
363
+ };
364
+ function defaultLogger() {
365
+ return {
366
+ log(level, namespace, message, fields) {
367
+ if (level === "error" || level === "warn") {
368
+ console.error(`[${level.toUpperCase()}] ${namespace}: ${message}`, fields ?? "");
369
+ }
370
+ }
371
+ };
372
+ }
373
+ var Scheduler = class {
374
+ tickIntervalMs;
375
+ clock;
376
+ timer;
377
+ logger;
378
+ tasks = /* @__PURE__ */ new Map();
379
+ timerHandle = null;
380
+ running = false;
381
+ constructor(opts = {}) {
382
+ this.tickIntervalMs = opts.tickIntervalMs ?? 1e3;
383
+ this.clock = opts.clock ?? realClock;
384
+ this.timer = opts.timer ?? realTimer;
385
+ this.logger = opts.logger ?? defaultLogger();
386
+ }
387
+ // ---------------------------------------------------------------------------
388
+ // Lifecycle
389
+ // ---------------------------------------------------------------------------
390
+ /**
391
+ * Start the scheduler's internal tick loop.
392
+ * Calling `start()` while already running is a no-op.
393
+ */
394
+ start() {
395
+ if (this.running) return;
396
+ this.running = true;
397
+ this.timerHandle = this.timer.setInterval(() => {
398
+ this.tick(this.clock.now());
399
+ }, this.tickIntervalMs);
400
+ }
401
+ /**
402
+ * Stop the scheduler. All in-flight callbacks are allowed to complete
403
+ * (they are not aborted), but no new runs will be triggered.
404
+ * Calling `stop()` while not running is a no-op.
405
+ */
406
+ stop() {
407
+ if (!this.running) return;
408
+ this.running = false;
409
+ if (this.timerHandle !== null) {
410
+ this.timer.clearInterval(this.timerHandle);
411
+ this.timerHandle = null;
412
+ }
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Task management
416
+ // ---------------------------------------------------------------------------
417
+ /**
418
+ * Register a scheduled task. If a task with the same `id` already exists,
419
+ * it is replaced (the old state is discarded).
420
+ *
421
+ * For cron tasks, the first next-fire time is computed immediately from the
422
+ * current clock value.
423
+ */
424
+ add(task) {
425
+ const now = this.clock.now();
426
+ let nextFireAt = null;
427
+ if (task.schedule.kind === "cron") {
428
+ validateCronExpression(task.schedule.expression);
429
+ nextFireAt = computeNextCron(task.schedule, now, this.logger);
430
+ }
431
+ this.tasks.set(task.id, {
432
+ task,
433
+ lastFiredAt: now,
434
+ nextFireAt,
435
+ inFlight: false
436
+ });
437
+ }
438
+ /**
439
+ * Remove a registered task by id.
440
+ * Any in-flight callback for this task will be allowed to complete, but no
441
+ * further fires will occur.
442
+ * Returns `true` if the task was found and removed, `false` otherwise.
443
+ */
444
+ remove(id) {
445
+ return this.tasks.delete(id);
446
+ }
447
+ /** Returns the ids of all currently registered tasks. */
448
+ taskIds() {
449
+ return [...this.tasks.keys()];
450
+ }
451
+ // ---------------------------------------------------------------------------
452
+ // Tick — can be called manually in tests
453
+ // ---------------------------------------------------------------------------
454
+ /**
455
+ * Evaluate all registered tasks against the given timestamp and fire any
456
+ * that are due.
457
+ *
458
+ * This is the core scheduling method. The `start()` loop calls it
459
+ * automatically at `tickIntervalMs` granularity, but tests can call it
460
+ * directly to drive time without real timers.
461
+ */
462
+ tick(now) {
463
+ for (const state of this.tasks.values()) {
464
+ if (isDue(state, now)) {
465
+ this._fire(state, now);
466
+ }
467
+ }
468
+ }
469
+ // ---------------------------------------------------------------------------
470
+ // Internal fire
471
+ // ---------------------------------------------------------------------------
472
+ _fire(state, now) {
473
+ if (state.inFlight) {
474
+ this.logger.log("debug", "scheduler", "task skipped (overlap)", {
475
+ taskId: state.task.id,
476
+ now
477
+ });
478
+ return;
479
+ }
480
+ state.inFlight = true;
481
+ state.lastFiredAt = now;
482
+ if (state.task.schedule.kind === "cron") {
483
+ state.nextFireAt = computeNextCron(state.task.schedule, now, this.logger);
484
+ }
485
+ const ctx = { taskId: state.task.id, triggeredAt: now };
486
+ Promise.resolve().then(() => state.task.run(ctx)).catch((err) => {
487
+ const msg = err instanceof Error ? err.message : String(err);
488
+ this.logger.log("error", "scheduler", "task run failed", {
489
+ taskId: state.task.id,
490
+ error: msg,
491
+ stack: err instanceof Error ? err.stack : void 0
492
+ });
493
+ }).finally(() => {
494
+ state.inFlight = false;
495
+ });
496
+ }
497
+ };
498
+ function isDue(state, now) {
499
+ const { task } = state;
500
+ if (task.schedule.kind === "interval") {
501
+ return now >= state.lastFiredAt + task.schedule.everyMs;
502
+ }
503
+ if (state.nextFireAt === null) return false;
504
+ return now >= state.nextFireAt;
505
+ }
506
+ function validateCronExpression(expression) {
507
+ try {
508
+ CronExpressionParser.parse(expression, { tz: "UTC" });
509
+ } catch (err) {
510
+ const detail = err instanceof Error ? err.message : String(err);
511
+ throw new Error(
512
+ `Invalid cron expression "${expression}": ${detail}`
513
+ );
514
+ }
515
+ }
516
+ function computeNextCron(schedule, afterMs, logger) {
517
+ try {
518
+ const expr = CronExpressionParser.parse(schedule.expression, {
519
+ currentDate: new Date(afterMs),
520
+ tz: schedule.tz ?? "UTC"
521
+ });
522
+ return expr.next().getTime();
523
+ } catch (err) {
524
+ logger.log("error", "scheduler", "failed to compute next cron time", {
525
+ expression: schedule.expression,
526
+ error: err instanceof Error ? err.message : String(err)
527
+ });
528
+ return null;
529
+ }
530
+ }
531
+
532
+ // src/batch-runner.ts
533
+ var ConcurrentAgentBackend = class {
534
+ constructor(agent) {
535
+ this.agent = agent;
536
+ }
537
+ agent;
538
+ async run(item, signal) {
539
+ try {
540
+ let finalOutput = "";
541
+ let finalUsage = { inputTokens: 0, outputTokens: 0 };
542
+ let finalCost;
543
+ for await (const ev of this.agent.query(item.input, {
544
+ sessionId: item.sessionId,
545
+ ...item.userId ? { userId: item.userId } : {},
546
+ ...item.orgId ? { orgId: item.orgId } : {},
547
+ signal
548
+ })) {
549
+ if (ev.type === "result") {
550
+ finalOutput = typeof ev.output === "string" ? ev.output : ev.output == null ? "" : String(ev.output);
551
+ finalUsage = ev.usage;
552
+ finalCost = ev.cost;
553
+ }
554
+ }
555
+ return {
556
+ status: "success",
557
+ id: item.id,
558
+ output: finalOutput,
559
+ usage: finalUsage,
560
+ cost: finalCost,
561
+ sessionId: item.sessionId
562
+ };
563
+ } catch (err) {
564
+ return {
565
+ status: "error",
566
+ id: item.id,
567
+ error: err instanceof Error ? err.message : String(err),
568
+ sessionId: item.sessionId
569
+ };
570
+ }
571
+ }
572
+ };
573
+ var BatchRunner = class {
574
+ concurrency;
575
+ backend;
576
+ constructor(agent, options = {}) {
577
+ this.concurrency = Math.max(1, options.concurrency ?? 4);
578
+ this.backend = options.backend ?? new ConcurrentAgentBackend(agent);
579
+ }
580
+ /**
581
+ * Process a list of inputs with bounded concurrency.
582
+ *
583
+ * @param items - Items to process. Each must have at least `input` set.
584
+ * @param opts - Run-level options (signal, progress callback, collectResults).
585
+ * @returns `BatchResult` containing per-item outcomes and aggregate totals.
586
+ *
587
+ * ### Large-batch tip
588
+ * For very large batches (thousands of items), holding all results in memory may
589
+ * be impractical. Pass `collectResults: false` to skip in-memory accumulation:
590
+ * `BatchResult.results` will be empty while `aggregate` totals remain accurate.
591
+ * Drain results incrementally via `onProgress` instead.
592
+ */
593
+ async run(items, opts = {}) {
594
+ const { signal, onProgress, collectResults = true } = opts;
595
+ const resolved = items.map((item, idx) => ({
596
+ id: item.id ?? String(idx),
597
+ input: item.input,
598
+ userId: item.userId ?? "",
599
+ orgId: item.orgId ?? "",
600
+ sessionId: item.sessionId ?? crypto.randomUUID()
601
+ }));
602
+ const results = [];
603
+ let cancelled = false;
604
+ let inlineTotalInputTokens = 0;
605
+ let inlineTotalOutputTokens = 0;
606
+ let inlineTotalUsd;
607
+ let inlineSuccessCount = 0;
608
+ let inlineErrorCount = 0;
609
+ if (resolved.length === 0) {
610
+ return { results, aggregate: computeAggregate(results, cancelled) };
611
+ }
612
+ let nextIndex = 0;
613
+ const processOne = async () => {
614
+ while (true) {
615
+ if (signal?.aborted) {
616
+ cancelled = true;
617
+ return;
618
+ }
619
+ const idx = nextIndex;
620
+ if (idx >= resolved.length) return;
621
+ nextIndex++;
622
+ const item = resolved[idx];
623
+ const itemSignal = signal ?? new AbortController().signal;
624
+ let result;
625
+ try {
626
+ result = await this.backend.run(item, itemSignal);
627
+ } catch (err) {
628
+ result = {
629
+ status: "error",
630
+ id: item.id,
631
+ error: err instanceof Error ? err.message : String(err),
632
+ sessionId: item.sessionId
633
+ };
634
+ }
635
+ if (collectResults) {
636
+ results.push(result);
637
+ } else {
638
+ if (result.status === "success") {
639
+ inlineSuccessCount++;
640
+ inlineTotalInputTokens += result.usage.inputTokens;
641
+ inlineTotalOutputTokens += result.usage.outputTokens;
642
+ if (result.cost?.usd !== void 0) {
643
+ inlineTotalUsd = (inlineTotalUsd ?? 0) + result.cost.usd;
644
+ }
645
+ } else {
646
+ inlineErrorCount++;
647
+ }
648
+ }
649
+ onProgress?.(result);
650
+ }
651
+ };
652
+ const lanes = Array.from(
653
+ { length: Math.min(this.concurrency, resolved.length) },
654
+ () => processOne()
655
+ );
656
+ await Promise.all(lanes);
657
+ if (signal?.aborted) cancelled = true;
658
+ if (collectResults) {
659
+ return { results, aggregate: computeAggregate(results, cancelled) };
660
+ }
661
+ return {
662
+ results,
663
+ aggregate: {
664
+ totalUsage: { inputTokens: inlineTotalInputTokens, outputTokens: inlineTotalOutputTokens },
665
+ ...inlineTotalUsd !== void 0 ? { totalUsd: inlineTotalUsd } : {},
666
+ successCount: inlineSuccessCount,
667
+ errorCount: inlineErrorCount,
668
+ cancelled
669
+ }
670
+ };
671
+ }
672
+ };
673
+ function computeAggregate(results, cancelled) {
674
+ let totalInputTokens = 0;
675
+ let totalOutputTokens = 0;
676
+ let totalUsd;
677
+ let successCount = 0;
678
+ let errorCount = 0;
679
+ for (const r of results) {
680
+ if (r.status === "success") {
681
+ successCount++;
682
+ totalInputTokens += r.usage.inputTokens;
683
+ totalOutputTokens += r.usage.outputTokens;
684
+ if (r.cost?.usd !== void 0) {
685
+ totalUsd = (totalUsd ?? 0) + r.cost.usd;
686
+ }
687
+ } else {
688
+ errorCount++;
689
+ }
690
+ }
691
+ return {
692
+ totalUsage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
693
+ ...totalUsd !== void 0 ? { totalUsd } : {},
694
+ successCount,
695
+ errorCount,
696
+ cancelled
697
+ };
698
+ }
699
+
700
+ // src/index.ts
701
+ import { WorkflowRunError as WorkflowRunError2 } from "@eidentic/workflow";
702
+ var AsyncRunRegistry = class {
703
+ runs = /* @__PURE__ */ new Map();
704
+ maxRuns;
705
+ constructor(options) {
706
+ this.maxRuns = options?.maxRuns ?? 1e3;
707
+ }
708
+ set(entry) {
709
+ if (this.runs.size >= this.maxRuns) {
710
+ this._evictOldestSettled();
711
+ }
712
+ this.runs.set(entry.runId, entry);
713
+ }
714
+ get(runId) {
715
+ return this.runs.get(runId);
716
+ }
717
+ settle(runId, patch) {
718
+ const entry = this.runs.get(runId);
719
+ if (entry) {
720
+ Object.assign(entry, patch, { settledAt: Date.now() });
721
+ }
722
+ }
723
+ /** Evict the single oldest settled (non-in-flight) entry to make room. */
724
+ _evictOldestSettled() {
725
+ let oldestId;
726
+ let oldestAt = Infinity;
727
+ for (const [id, e] of this.runs) {
728
+ if (e.status !== "running" && e.createdAt < oldestAt) {
729
+ oldestAt = e.createdAt;
730
+ oldestId = id;
731
+ }
732
+ }
733
+ if (oldestId !== void 0) {
734
+ this.runs.delete(oldestId);
735
+ }
736
+ }
737
+ /** Return all entries (copy of values). Used by graceful drain to check in-flight count. */
738
+ values() {
739
+ return [...this.runs.values()];
740
+ }
741
+ };
742
+ var NoAuth = {
743
+ authenticate(_req) {
744
+ return {};
745
+ }
746
+ };
747
+ function ApiKeyAuth(keys) {
748
+ return {
749
+ authenticate(req) {
750
+ const authHeader = req.headers["authorization"];
751
+ const xApiKey = req.headers["x-api-key"];
752
+ let key;
753
+ if (authHeader?.startsWith("Bearer ")) {
754
+ key = authHeader.slice(7);
755
+ } else if (xApiKey) {
756
+ key = xApiKey;
757
+ }
758
+ if (!key) return null;
759
+ if (!Object.hasOwn(keys, key)) return null;
760
+ return keys[key] ?? null;
761
+ }
762
+ };
763
+ }
764
+ function makeResolver(agents) {
765
+ if (typeof agents === "function") return agents;
766
+ return (id) => agents[id];
767
+ }
768
+ async function runAuth(auth, req) {
769
+ const headers = {};
770
+ req.headers.forEach((value, key) => {
771
+ headers[key.toLowerCase()] = value;
772
+ });
773
+ const url = new URL(req.url);
774
+ const authReq = {
775
+ method: req.method,
776
+ path: url.pathname,
777
+ headers
778
+ };
779
+ return auth.authenticate(authReq);
780
+ }
781
+ var BODY_LIMIT = 512 * 1024;
782
+ var STREAM_EVENT_TYPES_THAT_PERSIST = /* @__PURE__ */ new Set([
783
+ "assistant",
784
+ "tool.result",
785
+ "compaction"
786
+ ]);
787
+ function makeSseIdTracker(baseSeq) {
788
+ let next = baseSeq;
789
+ return {
790
+ idForSessionInit() {
791
+ return String(next);
792
+ },
793
+ idForPersistedEvent() {
794
+ next += 1;
795
+ return String(next);
796
+ },
797
+ currentId() {
798
+ return String(next);
799
+ }
800
+ };
801
+ }
802
+ function synthesizeResultFromStore(storedEvents, sessionId) {
803
+ for (let i = storedEvents.length - 1; i >= 0; i--) {
804
+ const ev = storedEvents[i];
805
+ if (ev.kind === "assistant") {
806
+ const payload = ev.payload;
807
+ const content = payload.content ?? [];
808
+ const hasToolUse = content.some((b) => b.type === "tool_use");
809
+ if (hasToolUse) {
810
+ return null;
811
+ }
812
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
813
+ const usage = ev.meta?.usage ?? {
814
+ inputTokens: 0,
815
+ outputTokens: 0
816
+ };
817
+ return {
818
+ type: "result",
819
+ subtype: "success",
820
+ output: text,
821
+ usage,
822
+ numTurns: storedEvents.filter((e) => e.kind === "assistant").length,
823
+ sessionId
824
+ };
825
+ }
826
+ if (ev.kind === "suspension") {
827
+ return null;
828
+ }
829
+ }
830
+ return null;
831
+ }
832
+ function storedEventToStreamPayload(ev) {
833
+ switch (ev.kind) {
834
+ case "assistant": {
835
+ const payload = ev.payload;
836
+ return { type: "assistant", content: payload.content ?? [] };
837
+ }
838
+ case "tool_result": {
839
+ const payload = ev.payload;
840
+ return {
841
+ type: "tool.result",
842
+ callId: payload.callId,
843
+ toolName: payload.toolName,
844
+ output: payload.output,
845
+ isError: false
846
+ };
847
+ }
848
+ case "compaction": {
849
+ const payload = ev.payload;
850
+ return {
851
+ type: "compaction",
852
+ sessionId: ev.sessionId,
853
+ before: payload.before,
854
+ after: payload.after,
855
+ stages: payload.stages
856
+ };
857
+ }
858
+ // "user", "checkpoint", "tool_call", "suspension" — not replayed
859
+ default:
860
+ return null;
861
+ }
862
+ }
863
+ function assertCallbackUrl(rawUrl, allowPrivateHosts) {
864
+ let parsed;
865
+ try {
866
+ parsed = new URL(rawUrl);
867
+ } catch {
868
+ throw new Error("Invalid callbackUrl");
869
+ }
870
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
871
+ throw new Error("callbackUrl must use http or https");
872
+ }
873
+ if (!allowPrivateHosts && isCallbackHostBlocked(parsed.hostname)) {
874
+ throw new Error("callbackUrl resolves to a blocked private/loopback/metadata host");
875
+ }
876
+ return parsed;
877
+ }
878
+ function isCallbackHostBlocked(host) {
879
+ const h = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
880
+ if (h.toLowerCase() === "localhost") return true;
881
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
882
+ if (v4) {
883
+ const a = Number(v4[1]), b = Number(v4[2]);
884
+ if (a === 0 || a === 127 || a === 10) return true;
885
+ if (a === 172 && b >= 16 && b <= 31) return true;
886
+ if (a === 192 && b === 168) return true;
887
+ if (a === 169 && b === 254) return true;
888
+ return false;
889
+ }
890
+ let ndInt;
891
+ if (/^0x[0-9a-fA-F]+$/.test(h)) ndInt = parseInt(h, 16) >>> 0;
892
+ else if (/^\d+$/.test(h)) {
893
+ const n = Number(h);
894
+ if (!isNaN(n) && n >= 0 && n <= 4294967295) ndInt = n >>> 0;
895
+ } else if (/^0[0-7]+$/.test(h)) ndInt = parseInt(h, 8) >>> 0;
896
+ if (ndInt !== void 0) {
897
+ const a = ndInt >>> 24 & 255, b = ndInt >>> 16 & 255;
898
+ if (a === 0 || a === 127 || a === 10) return true;
899
+ if (a === 172 && b >= 16 && b <= 31) return true;
900
+ if (a === 192 && b === 168) return true;
901
+ if (a === 169 && b === 254) return true;
902
+ return false;
903
+ }
904
+ let v6 = h.toLowerCase();
905
+ const pct = v6.indexOf("%");
906
+ if (pct !== -1) v6 = v6.slice(0, pct);
907
+ const mapD = /^::(?:ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(v6);
908
+ if (mapD) {
909
+ const a = Number(mapD[1]), b = Number(mapD[2]);
910
+ const n = (Number(mapD[1]) << 24 | Number(mapD[2]) << 16 | Number(mapD[3]) << 8 | Number(mapD[4])) >>> 0;
911
+ const aa = n >>> 24 & 255, bb = n >>> 16 & 255;
912
+ if (aa === 0 || aa === 127 || aa === 10) return true;
913
+ if (aa === 172 && bb >= 16 && bb <= 31) return true;
914
+ if (aa === 192 && bb === 168) return true;
915
+ if (aa === 169 && bb === 254) return true;
916
+ void a;
917
+ void b;
918
+ return false;
919
+ }
920
+ const mapH = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(v6);
921
+ if (mapH) {
922
+ const n = (parseInt(mapH[1] ?? "0", 16) << 16 | parseInt(mapH[2] ?? "0", 16)) >>> 0;
923
+ const a = n >>> 24 & 255, b = n >>> 16 & 255;
924
+ if (a === 0 || a === 127 || a === 10) return true;
925
+ if (a === 172 && b >= 16 && b <= 31) return true;
926
+ if (a === 192 && b === 168) return true;
927
+ if (a === 169 && b === 254) return true;
928
+ return false;
929
+ }
930
+ if (v6 === "::" || v6 === "::1") return true;
931
+ if (/^f[cd][0-9a-f]{0,2}(:|$)/.test(v6)) return true;
932
+ if (/^fe[89ab][0-9a-f]?(:|$)/.test(v6)) return true;
933
+ return false;
934
+ }
935
+ async function deliverWebhook(callbackUrl, payload, signingSecret, logger) {
936
+ const body = JSON.stringify(payload);
937
+ const timestamp = String(Date.now());
938
+ const message = timestamp + "." + body;
939
+ const enc = new TextEncoder();
940
+ const key = await crypto.subtle.importKey(
941
+ "raw",
942
+ enc.encode(signingSecret),
943
+ { name: "HMAC", hash: "SHA-256" },
944
+ false,
945
+ ["sign"]
946
+ );
947
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
948
+ const hexSig = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
949
+ const signature = `sha256=${hexSig}`;
950
+ const delays = [0, 1e3, 2e3];
951
+ for (let attempt = 0; attempt < delays.length; attempt++) {
952
+ if (attempt > 0) {
953
+ await new Promise((r) => setTimeout(r, delays[attempt]));
954
+ }
955
+ try {
956
+ const controller = new AbortController();
957
+ const timer = setTimeout(() => controller.abort(), 1e4);
958
+ try {
959
+ const res = await fetch(callbackUrl, {
960
+ method: "POST",
961
+ headers: {
962
+ "content-type": "application/json",
963
+ "X-Eidentic-Signature": signature,
964
+ "X-Eidentic-Timestamp": timestamp
965
+ },
966
+ body,
967
+ signal: controller.signal,
968
+ // Never follow redirects
969
+ redirect: "manual"
970
+ });
971
+ clearTimeout(timer);
972
+ if (res.status >= 200 && res.status < 300) return;
973
+ logger.error(`[eidentic/server] webhook delivery attempt ${attempt + 1} failed: HTTP ${res.status} for ${callbackUrl}`);
974
+ } finally {
975
+ clearTimeout(timer);
976
+ }
977
+ } catch (err) {
978
+ logger.error(`[eidentic/server] webhook delivery attempt ${attempt + 1} error:`, err);
979
+ }
980
+ }
981
+ logger.error(`[eidentic/server] webhook delivery exhausted retries for ${callbackUrl}`);
982
+ }
983
+ var DEFAULT_PRE_AUTH_CAPACITY = 60;
984
+ var DEFAULT_PRE_AUTH_REFILL_PER_SEC = 1;
985
+ function defaultGetClientKey(c, trustProxy) {
986
+ if (trustProxy) {
987
+ const xff = c.req.header("x-forwarded-for");
988
+ if (xff) {
989
+ const first = xff.split(",")[0]?.trim();
990
+ if (first) return first;
991
+ }
992
+ }
993
+ const incoming = c.env?.["incoming"];
994
+ return incoming?.socket?.remoteAddress ?? "unknown";
995
+ }
996
+ function createServer(opts) {
997
+ const resolve = makeResolver(opts.agents);
998
+ const auth = opts.auth ?? NoAuth;
999
+ const defaultKey = (p, _agentId) => p.apiKey ?? p.userId ?? p.orgId ?? "anonymous";
1000
+ const getRateLimitKey = opts.rateLimitKey ?? defaultKey;
1001
+ const getQuotaKey = opts.quotaKey ?? defaultKey;
1002
+ const maxInputChars = opts.maxInputChars ?? 32e3;
1003
+ const quota = opts.quota;
1004
+ const preAuthLimiter = opts.preAuthRateLimiter === null ? null : opts.preAuthRateLimiter ?? new InMemoryTokenBucketLimiter({
1005
+ capacity: DEFAULT_PRE_AUTH_CAPACITY,
1006
+ refillPerSec: DEFAULT_PRE_AUTH_REFILL_PER_SEC
1007
+ });
1008
+ const trustProxy = opts.trustProxy ?? false;
1009
+ const getClientKey = opts.getClientKey ? opts.getClientKey : (c) => defaultGetClientKey(c, trustProxy);
1010
+ let _draining = false;
1011
+ const app = new Hono({ strict: true });
1012
+ const base = opts.basePath ?? "";
1013
+ const r = base ? app.basePath(base) : app;
1014
+ if (opts.cors !== void 0) {
1015
+ r.use("*", cors(opts.cors));
1016
+ }
1017
+ r.use("*", async (c, next) => {
1018
+ await next();
1019
+ c.header("X-Content-Type-Options", "nosniff");
1020
+ });
1021
+ r.use("/v1/*", async (c, next) => {
1022
+ if (_draining) {
1023
+ c.header("Retry-After", "5");
1024
+ return c.json({ error: "service_draining" }, 503);
1025
+ }
1026
+ await next();
1027
+ });
1028
+ if (preAuthLimiter !== null) {
1029
+ r.use("/v1/*", async (c, next) => {
1030
+ const clientKey = getClientKey(c);
1031
+ const rl = await preAuthLimiter.acquire(clientKey);
1032
+ if (!rl.ok) {
1033
+ c.header("X-Content-Type-Options", "nosniff");
1034
+ const retryAfterSec = rl.retryAfterMs !== void 0 ? Math.ceil(rl.retryAfterMs / 1e3) : void 0;
1035
+ if (retryAfterSec !== void 0) {
1036
+ c.header("Retry-After", String(retryAfterSec));
1037
+ }
1038
+ return c.json({ error: "rate_limited", retryAfterMs: rl.retryAfterMs }, 429);
1039
+ }
1040
+ await next();
1041
+ });
1042
+ }
1043
+ const asyncRuns = new AsyncRunRegistry({ maxRuns: opts.maxAsyncRuns });
1044
+ const workflowRuns = opts.workflowRuns ?? createWorkflowRunRegistry();
1045
+ const _setDraining = (v) => {
1046
+ _draining = v;
1047
+ };
1048
+ const _getAsyncRuns = () => asyncRuns;
1049
+ r.get("/health", (c) => c.json({ ok: true }));
1050
+ function checkOwnership(session, principal) {
1051
+ const sessionOwned = session.userId !== void 0 || session.orgId !== void 0 || session.apiKey !== void 0;
1052
+ if (!sessionOwned) return true;
1053
+ if (session.userId !== void 0 && principal.userId === session.userId) return true;
1054
+ if (session.orgId !== void 0 && principal.orgId === session.orgId) return true;
1055
+ if (session.apiKey !== void 0 && principal.apiKey === session.apiKey) return true;
1056
+ return false;
1057
+ }
1058
+ async function runAgentStream(c, agent, agentId, principal, sessionId, getIterable, logTag, emitSessionComment) {
1059
+ const rawLastEventId = c.req.header("last-event-id");
1060
+ let lastEventId;
1061
+ if (rawLastEventId !== void 0) {
1062
+ const parsed = parseInt(rawLastEventId, 10);
1063
+ if (isNaN(parsed) || parsed < 0 || !Number.isSafeInteger(parsed)) {
1064
+ return c.json({ error: "Invalid Last-Event-ID: must be a non-negative integer" }, 400);
1065
+ }
1066
+ lastEventId = parsed;
1067
+ }
1068
+ const hasLastEventId = lastEventId !== void 0;
1069
+ let streamQuotaKey;
1070
+ let streamQuotaReservation;
1071
+ if (quota) {
1072
+ streamQuotaKey = getQuotaKey(principal, agentId);
1073
+ const qc = await quota.check(streamQuotaKey);
1074
+ if (!qc.ok) {
1075
+ return c.json({ error: "quota_exceeded", reason: qc.reason, usage: qc.usage }, 402);
1076
+ }
1077
+ if (qc.warn) c.header("X-Eidentic-Quota-Warning", "soft-limit");
1078
+ streamQuotaReservation = qc.reservation;
1079
+ }
1080
+ const signal = c.req.raw.signal ?? new AbortController().signal;
1081
+ return streamSSE(c, async (stream) => {
1082
+ if (emitSessionComment) {
1083
+ await stream.writeln(`: session=${sessionId.replace(/[\r\n]/g, "")}`);
1084
+ }
1085
+ let storedEventsCache = null;
1086
+ if (hasLastEventId) {
1087
+ const storedEvents = await agent.store.readEvents(sessionId);
1088
+ storedEventsCache = storedEvents;
1089
+ const toReplay = storedEvents.filter((e) => e.seq > lastEventId);
1090
+ for (const ev of toReplay) {
1091
+ if (signal.aborted) break;
1092
+ const payload = storedEventToStreamPayload(ev);
1093
+ if (payload !== null) {
1094
+ await stream.writeSSE({
1095
+ event: payload["type"],
1096
+ data: JSON.stringify(payload),
1097
+ id: String(ev.seq)
1098
+ });
1099
+ }
1100
+ }
1101
+ const syntheticResult = synthesizeResultFromStore(storedEvents, sessionId);
1102
+ if (syntheticResult !== null) {
1103
+ await stream.writeSSE({
1104
+ event: "result",
1105
+ data: JSON.stringify(syntheticResult)
1106
+ });
1107
+ if (quota && streamQuotaReservation !== void 0) {
1108
+ quota.release?.(streamQuotaReservation);
1109
+ }
1110
+ return;
1111
+ }
1112
+ }
1113
+ const existingEvents = storedEventsCache ?? await agent.store.readEvents(sessionId);
1114
+ const baseSeq = existingEvents.length === 0 ? 0 : existingEvents[existingEvents.length - 1].seq + 1;
1115
+ const idTracker = makeSseIdTracker(baseSeq);
1116
+ let terminalResult;
1117
+ try {
1118
+ for await (const ev of getIterable()) {
1119
+ if (signal.aborted) break;
1120
+ if (ev.type === "session.init") {
1121
+ await stream.writeSSE({
1122
+ event: ev.type,
1123
+ data: JSON.stringify(ev),
1124
+ id: idTracker.idForSessionInit()
1125
+ });
1126
+ } else if (STREAM_EVENT_TYPES_THAT_PERSIST.has(ev.type)) {
1127
+ await stream.writeSSE({
1128
+ event: ev.type,
1129
+ data: JSON.stringify(ev),
1130
+ id: idTracker.idForPersistedEvent()
1131
+ });
1132
+ } else {
1133
+ let payload = ev;
1134
+ if (ev.type === "result" && ev.subtype === "error") {
1135
+ console.error(`[eidentic/server] ${logTag} run error:`, ev.output);
1136
+ payload = { ...ev, output: "Agent run failed" };
1137
+ }
1138
+ await stream.writeSSE({ event: ev.type, data: JSON.stringify(payload) });
1139
+ }
1140
+ if (ev.type === "result") {
1141
+ terminalResult = { usage: ev.usage, cost: ev.cost };
1142
+ }
1143
+ }
1144
+ } catch (err) {
1145
+ if (quota && streamQuotaReservation !== void 0) {
1146
+ quota.release?.(streamQuotaReservation);
1147
+ streamQuotaReservation = void 0;
1148
+ }
1149
+ if (!signal.aborted) {
1150
+ console.error(`[eidentic/server] ${logTag} error:`, err);
1151
+ await stream.writeSSE({
1152
+ event: "result",
1153
+ data: JSON.stringify({
1154
+ type: "result",
1155
+ subtype: "error",
1156
+ output: "Agent run failed",
1157
+ usage: { inputTokens: 0, outputTokens: 0 },
1158
+ numTurns: 0,
1159
+ sessionId
1160
+ })
1161
+ });
1162
+ }
1163
+ }
1164
+ if (quota && streamQuotaKey !== void 0 && terminalResult) {
1165
+ const tokens = terminalResult.usage.inputTokens + terminalResult.usage.outputTokens;
1166
+ const usd = terminalResult.cost?.usd ?? 0;
1167
+ await quota.record(streamQuotaKey, { usd, tokens }, streamQuotaReservation);
1168
+ } else if (quota && streamQuotaReservation !== void 0 && !terminalResult) {
1169
+ quota.release?.(streamQuotaReservation);
1170
+ }
1171
+ });
1172
+ }
1173
+ async function checkPostAuthRateLimit(c, principal, agentId) {
1174
+ if (!opts.rateLimiter) return null;
1175
+ const baseKey = getRateLimitKey(principal, agentId);
1176
+ const rlKey = baseKey === "anonymous" ? `anon:${getClientKey(c)}` : baseKey;
1177
+ const rl = await opts.rateLimiter.acquire(rlKey);
1178
+ if (!rl.ok) {
1179
+ const retryAfterSec = rl.retryAfterMs !== void 0 ? Math.ceil(rl.retryAfterMs / 1e3) : void 0;
1180
+ if (retryAfterSec !== void 0) c.header("Retry-After", String(retryAfterSec));
1181
+ return c.json({ error: "rate_limited", retryAfterMs: rl.retryAfterMs }, 429);
1182
+ }
1183
+ return null;
1184
+ }
1185
+ r.post(
1186
+ "/v1/agents/:agentId/query",
1187
+ bodyLimit({ maxSize: BODY_LIMIT }),
1188
+ async (c) => {
1189
+ const principal = await runAuth(auth, c.req.raw);
1190
+ if (principal === null) return c.json({ error: "Unauthorized" }, 401);
1191
+ const agentId = c.req.param("agentId");
1192
+ const rlErr = await checkPostAuthRateLimit(c, principal, agentId);
1193
+ if (rlErr) return rlErr;
1194
+ let body;
1195
+ try {
1196
+ body = await c.req.json();
1197
+ } catch {
1198
+ return c.json({ error: "Invalid JSON body" }, 400);
1199
+ }
1200
+ if (typeof body !== "object" || body === null || typeof body["input"] !== "string" || body["input"] === "") {
1201
+ return c.json({ error: "Missing or invalid 'input' field" }, 400);
1202
+ }
1203
+ const { input, sessionId: bodySessionId } = body;
1204
+ if (input.length > maxInputChars) {
1205
+ return c.json({ error: `Input exceeds maximum length of ${maxInputChars} characters` }, 400);
1206
+ }
1207
+ const agent = resolve(agentId);
1208
+ if (!agent) {
1209
+ return c.json({ error: "Not found" }, 404);
1210
+ }
1211
+ const sessionId = typeof bodySessionId === "string" && bodySessionId.length > 0 ? bodySessionId : crypto.randomUUID();
1212
+ if (typeof bodySessionId === "string" && bodySessionId.length > 0) {
1213
+ const sessionRecord = await agent.store.getSession(bodySessionId);
1214
+ if (sessionRecord && !checkOwnership(sessionRecord, principal)) {
1215
+ return c.json({ error: "Forbidden" }, 403);
1216
+ }
1217
+ }
1218
+ return runAgentStream(
1219
+ c,
1220
+ agent,
1221
+ agentId,
1222
+ principal,
1223
+ sessionId,
1224
+ () => agent.query(input, {
1225
+ sessionId,
1226
+ userId: principal.userId,
1227
+ orgId: principal.orgId,
1228
+ // H1 fix: pass apiKey so apiKey-only principals own their sessions.
1229
+ apiKey: principal.apiKey,
1230
+ signal: c.req.raw.signal ?? new AbortController().signal
1231
+ }),
1232
+ "agent.query",
1233
+ /* emitSessionComment */
1234
+ true
1235
+ );
1236
+ }
1237
+ );
1238
+ r.post(
1239
+ "/v1/agents/:agentId/resume",
1240
+ bodyLimit({ maxSize: BODY_LIMIT }),
1241
+ async (c) => {
1242
+ const principal = await runAuth(auth, c.req.raw);
1243
+ if (principal === null) return c.json({ error: "Unauthorized" }, 401);
1244
+ const agentId = c.req.param("agentId");
1245
+ const rlErr = await checkPostAuthRateLimit(c, principal, agentId);
1246
+ if (rlErr) return rlErr;
1247
+ let body;
1248
+ try {
1249
+ body = await c.req.json();
1250
+ } catch {
1251
+ return c.json({ error: "Invalid JSON body" }, 400);
1252
+ }
1253
+ if (typeof body !== "object" || body === null || typeof body["sessionId"] !== "string" || body["sessionId"] === "") {
1254
+ return c.json({ error: "Missing or invalid 'sessionId' field" }, 400);
1255
+ }
1256
+ const rawDecision = body["decision"];
1257
+ if (typeof rawDecision === "string" && rawDecision.length > maxInputChars) {
1258
+ return c.json({ error: `Decision input exceeds maximum length of ${maxInputChars} characters` }, 400);
1259
+ }
1260
+ const { sessionId, decision } = body;
1261
+ const agent = resolve(agentId);
1262
+ if (!agent) {
1263
+ return c.json({ error: "Not found" }, 404);
1264
+ }
1265
+ const sessionRecord = await agent.store.getSession(sessionId);
1266
+ if (sessionRecord && !checkOwnership(sessionRecord, principal)) {
1267
+ return c.json({ error: "Forbidden" }, 403);
1268
+ }
1269
+ return runAgentStream(
1270
+ c,
1271
+ agent,
1272
+ agentId,
1273
+ principal,
1274
+ sessionId,
1275
+ () => agent.resume(sessionId, {
1276
+ userId: principal.userId,
1277
+ orgId: principal.orgId,
1278
+ decision,
1279
+ signal: c.req.raw.signal ?? new AbortController().signal
1280
+ }),
1281
+ "agent.resume",
1282
+ /* emitSessionComment */
1283
+ false
1284
+ );
1285
+ }
1286
+ );
1287
+ r.post(
1288
+ "/v1/agents/:agentId/runs",
1289
+ bodyLimit({ maxSize: BODY_LIMIT }),
1290
+ async (c) => {
1291
+ const principal = await runAuth(auth, c.req.raw);
1292
+ if (principal === null) return c.json({ error: "Unauthorized" }, 401);
1293
+ const agentId = c.req.param("agentId");
1294
+ const rlErr = await checkPostAuthRateLimit(c, principal, agentId);
1295
+ if (rlErr) return rlErr;
1296
+ let body;
1297
+ try {
1298
+ body = await c.req.json();
1299
+ } catch {
1300
+ return c.json({ error: "Invalid JSON body" }, 400);
1301
+ }
1302
+ if (typeof body !== "object" || body === null || typeof body["input"] !== "string" || body["input"] === "") {
1303
+ return c.json({ error: "Missing or invalid 'input' field" }, 400);
1304
+ }
1305
+ const { input, sessionId: bodySessionId, callbackUrl: rawCallbackUrl } = body;
1306
+ if (input.length > maxInputChars) {
1307
+ return c.json({ error: `Input exceeds maximum length of ${maxInputChars} characters` }, 400);
1308
+ }
1309
+ let validatedCallbackUrl;
1310
+ if (rawCallbackUrl !== void 0) {
1311
+ if (!opts.webhooks) {
1312
+ return c.json({ error: "Webhook callbacks are not configured on this server" }, 400);
1313
+ }
1314
+ if (typeof rawCallbackUrl !== "string" || rawCallbackUrl.length === 0) {
1315
+ return c.json({ error: "callbackUrl must be a non-empty string" }, 400);
1316
+ }
1317
+ try {
1318
+ const u = assertCallbackUrl(rawCallbackUrl, opts.webhooks.allowPrivateHosts ?? false);
1319
+ validatedCallbackUrl = u.href;
1320
+ } catch (err) {
1321
+ return c.json({ error: err instanceof Error ? err.message : "Invalid callbackUrl" }, 400);
1322
+ }
1323
+ }
1324
+ const agent = resolve(agentId);
1325
+ if (!agent) {
1326
+ return c.json({ error: "Not found" }, 404);
1327
+ }
1328
+ const sessionId = typeof bodySessionId === "string" && bodySessionId.length > 0 ? bodySessionId : crypto.randomUUID();
1329
+ if (typeof bodySessionId === "string" && bodySessionId.length > 0) {
1330
+ const sessionRecord = await agent.store.getSession(bodySessionId);
1331
+ if (sessionRecord && !checkOwnership(sessionRecord, principal)) {
1332
+ return c.json({ error: "Forbidden" }, 403);
1333
+ }
1334
+ }
1335
+ let asyncQuotaKey;
1336
+ let asyncQuotaReservation;
1337
+ if (quota) {
1338
+ asyncQuotaKey = getQuotaKey(principal, agentId);
1339
+ const qc = await quota.check(asyncQuotaKey);
1340
+ if (!qc.ok) {
1341
+ return c.json({ error: "quota_exceeded", reason: qc.reason, usage: qc.usage }, 402);
1342
+ }
1343
+ asyncQuotaReservation = qc.reservation;
1344
+ }
1345
+ const runId = crypto.randomUUID();
1346
+ asyncRuns.set({
1347
+ runId,
1348
+ sessionId,
1349
+ agentId,
1350
+ status: "running",
1351
+ owner: {
1352
+ userId: principal.userId,
1353
+ orgId: principal.orgId,
1354
+ apiKey: principal.apiKey
1355
+ },
1356
+ createdAt: Date.now()
1357
+ });
1358
+ (async () => {
1359
+ let terminalOutput;
1360
+ let terminalError;
1361
+ let terminalUsage;
1362
+ let terminalResult;
1363
+ let localReservation = asyncQuotaReservation;
1364
+ try {
1365
+ for await (const ev of agent.query(input, {
1366
+ sessionId,
1367
+ userId: principal.userId,
1368
+ orgId: principal.orgId
1369
+ })) {
1370
+ if (ev.type === "result") {
1371
+ terminalResult = { usage: ev.usage, cost: ev.cost };
1372
+ terminalUsage = ev.usage;
1373
+ if (ev.subtype === "success") {
1374
+ terminalOutput = typeof ev.output === "string" ? ev.output : ev.output !== void 0 ? String(ev.output) : void 0;
1375
+ } else if (ev.subtype === "error") {
1376
+ terminalError = typeof ev.output === "string" ? ev.output : ev.output !== void 0 ? String(ev.output) : void 0;
1377
+ }
1378
+ }
1379
+ }
1380
+ if (quota && asyncQuotaKey !== void 0 && terminalResult) {
1381
+ const tokens = terminalResult.usage.inputTokens + terminalResult.usage.outputTokens;
1382
+ const usd = terminalResult.cost?.usd ?? 0;
1383
+ await quota.record(asyncQuotaKey, { usd, tokens }, localReservation);
1384
+ localReservation = void 0;
1385
+ }
1386
+ const finalStatus = terminalError ? "failed" : "completed";
1387
+ asyncRuns.settle(runId, {
1388
+ status: finalStatus,
1389
+ output: terminalOutput,
1390
+ error: terminalError
1391
+ });
1392
+ if (validatedCallbackUrl && opts.webhooks) {
1393
+ const webhookPayload = {
1394
+ runId,
1395
+ agentId,
1396
+ status: finalStatus,
1397
+ ...terminalOutput !== void 0 ? { output: terminalOutput } : {},
1398
+ ...terminalError !== void 0 ? { error: terminalError } : {},
1399
+ ...terminalUsage !== void 0 ? { usage: terminalUsage } : {}
1400
+ };
1401
+ void deliverWebhook(validatedCallbackUrl, webhookPayload, opts.webhooks.signingSecret, console);
1402
+ }
1403
+ } catch (err) {
1404
+ if (quota && localReservation !== void 0) {
1405
+ quota.release?.(localReservation);
1406
+ localReservation = void 0;
1407
+ }
1408
+ const msg = err instanceof Error ? err.message : String(err);
1409
+ asyncRuns.settle(runId, { status: "failed", error: msg });
1410
+ if (validatedCallbackUrl && opts.webhooks) {
1411
+ const webhookPayload = {
1412
+ runId,
1413
+ agentId,
1414
+ status: "failed",
1415
+ error: msg,
1416
+ ...terminalUsage !== void 0 ? { usage: terminalUsage } : {}
1417
+ };
1418
+ void deliverWebhook(validatedCallbackUrl, webhookPayload, opts.webhooks.signingSecret, console);
1419
+ }
1420
+ } finally {
1421
+ if (quota && localReservation !== void 0 && !terminalResult) {
1422
+ quota.release?.(localReservation);
1423
+ }
1424
+ }
1425
+ })();
1426
+ return c.json({ runId, sessionId, status: "running" }, 202);
1427
+ }
1428
+ );
1429
+ r.get("/v1/agents/:agentId/runs/:runId/status", async (c) => {
1430
+ const principal = await runAuth(auth, c.req.raw);
1431
+ if (principal === null) {
1432
+ return c.json({ error: "Unauthorized" }, 401);
1433
+ }
1434
+ const agentId = c.req.param("agentId");
1435
+ const agent = resolve(agentId);
1436
+ if (!agent) {
1437
+ return c.json({ error: "Not found" }, 404);
1438
+ }
1439
+ const runId = c.req.param("runId");
1440
+ const entry = asyncRuns.get(runId);
1441
+ if (!entry) {
1442
+ return c.json({ error: "Not found" }, 404);
1443
+ }
1444
+ const ownerMatches = entry.owner.userId !== void 0 && entry.owner.userId === principal.userId || entry.owner.orgId !== void 0 && entry.owner.orgId === principal.orgId || entry.owner.apiKey !== void 0 && entry.owner.apiKey === principal.apiKey || // NoAuth / anonymous: allow if owner has no identifying fields set
1445
+ entry.owner.userId === void 0 && entry.owner.orgId === void 0 && entry.owner.apiKey === void 0;
1446
+ if (!ownerMatches) {
1447
+ return c.json({ error: "Forbidden" }, 403);
1448
+ }
1449
+ const response = {
1450
+ runId: entry.runId,
1451
+ sessionId: entry.sessionId,
1452
+ status: entry.status
1453
+ };
1454
+ if (entry.output !== void 0) response["output"] = entry.output;
1455
+ if (entry.error !== void 0) response["error"] = entry.error;
1456
+ if (entry.settledAt !== void 0) response["settledAt"] = entry.settledAt;
1457
+ return c.json(response, 200);
1458
+ });
1459
+ if (opts.exposeEvents === true) {
1460
+ r.get("/v1/agents/:agentId/sessions/:sessionId/events", async (c) => {
1461
+ const principal = await runAuth(auth, c.req.raw);
1462
+ if (principal === null) {
1463
+ return c.json({ error: "Unauthorized" }, 401);
1464
+ }
1465
+ const agentId = c.req.param("agentId");
1466
+ const agent = resolve(agentId);
1467
+ if (!agent) {
1468
+ return c.json({ error: "Not found" }, 404);
1469
+ }
1470
+ const sessionId = c.req.param("sessionId");
1471
+ const sessionRecord = await agent.store.getSession(sessionId);
1472
+ if (sessionRecord && !checkOwnership(sessionRecord, principal)) {
1473
+ return c.json({ error: "Forbidden" }, 403);
1474
+ }
1475
+ const events = await agent.store.readEvents(sessionId);
1476
+ return c.json({ events });
1477
+ });
1478
+ }
1479
+ function checkWorkflowOwnership(rec, principal) {
1480
+ const owner = rec.owner;
1481
+ if (owner === void 0) return true;
1482
+ const hasAnyOwner = owner.userId !== void 0 || owner.orgId !== void 0 || owner.apiKey !== void 0;
1483
+ if (!hasAnyOwner) return true;
1484
+ if (owner.userId !== void 0 && principal.userId === owner.userId) return true;
1485
+ if (owner.orgId !== void 0 && principal.orgId === owner.orgId) return true;
1486
+ if (owner.apiKey !== void 0 && principal.apiKey === owner.apiKey) return true;
1487
+ return false;
1488
+ }
1489
+ r.get("/v1/workflows", async (c) => {
1490
+ const principal = await runAuth(auth, c.req.raw);
1491
+ if (principal === null) {
1492
+ return c.json({ error: "Unauthorized" }, 401);
1493
+ }
1494
+ const summaries = workflowRuns.list().filter((rec) => checkWorkflowOwnership(rec, principal)).map((rec) => ({
1495
+ id: rec.id,
1496
+ name: rec.name,
1497
+ status: rec.status,
1498
+ startedAt: rec.startedAt,
1499
+ durationMs: rec.durationMs,
1500
+ stepCount: rec.stepCount
1501
+ }));
1502
+ return c.json(summaries, 200);
1503
+ });
1504
+ r.get("/v1/workflows/:id", async (c) => {
1505
+ const principal = await runAuth(auth, c.req.raw);
1506
+ if (principal === null) {
1507
+ return c.json({ error: "Unauthorized" }, 401);
1508
+ }
1509
+ const id = c.req.param("id");
1510
+ const rec = workflowRuns.get(id);
1511
+ if (!rec || !checkWorkflowOwnership(rec, principal)) {
1512
+ return c.json({ error: "Not found" }, 404);
1513
+ }
1514
+ const detail = {
1515
+ id: rec.id,
1516
+ name: rec.name,
1517
+ status: rec.status,
1518
+ startedAt: rec.startedAt,
1519
+ durationMs: rec.durationMs,
1520
+ stepCount: rec.stepCount,
1521
+ trace: rec.trace,
1522
+ ...rec.output !== void 0 ? { output: rec.output } : {},
1523
+ ...rec.error !== void 0 ? { error: rec.error } : {}
1524
+ };
1525
+ return c.json(detail, 200);
1526
+ });
1527
+ const handle = {
1528
+ recordWorkflow(name, result, owner, opts2) {
1529
+ const rec = workflowRuns.record(name, result, owner, opts2);
1530
+ return rec.id;
1531
+ },
1532
+ recordWorkflowError(err, owner, opts2) {
1533
+ const msg = err.cause instanceof Error ? err.cause.message : String(err.cause ?? err.message);
1534
+ const rec = workflowRuns.recordError(err.workflowName, err.trace, msg, owner, opts2);
1535
+ return rec.id;
1536
+ }
1537
+ };
1538
+ Object.defineProperties(app, {
1539
+ _setDraining: { value: _setDraining, enumerable: false, writable: false },
1540
+ _getAsyncRuns: { value: _getAsyncRuns, enumerable: false, writable: false }
1541
+ });
1542
+ return Object.assign(app, { handle });
1543
+ }
1544
+ async function serveNode(app, opts) {
1545
+ let nodeServer;
1546
+ try {
1547
+ nodeServer = await import("./dist-VXER5V4E.js");
1548
+ } catch {
1549
+ throw new Error(
1550
+ "@hono/node-server is not installed. Run `pnpm add @hono/node-server` (or npm/yarn) in your project to use serveNode()."
1551
+ );
1552
+ }
1553
+ const port = opts?.port ?? 3e3;
1554
+ const server = nodeServer.serve({ fetch: app.fetch, port });
1555
+ const _setDraining = app._setDraining;
1556
+ const _getAsyncRuns = app._getAsyncRuns;
1557
+ return {
1558
+ close() {
1559
+ server.close();
1560
+ },
1561
+ async drain(timeoutMs = 3e4) {
1562
+ if (_setDraining) _setDraining(true);
1563
+ await new Promise((resolve) => server.close(() => resolve()));
1564
+ const deadline = Date.now() + timeoutMs;
1565
+ while (Date.now() < deadline) {
1566
+ if (_getAsyncRuns) {
1567
+ const running = _getAsyncRuns().values().filter((e) => e.status === "running");
1568
+ if (running.length === 0) break;
1569
+ } else {
1570
+ break;
1571
+ }
1572
+ await new Promise((r) => setTimeout(r, 100));
1573
+ }
1574
+ }
1575
+ };
1576
+ }
1577
+ export {
1578
+ ApiKeyAuth,
1579
+ AsyncRunRegistry,
1580
+ BatchRunner,
1581
+ InMemoryQuota,
1582
+ InMemoryTokenBucketLimiter,
1583
+ NoAuth,
1584
+ Scheduler,
1585
+ WorkflowRunError2 as WorkflowRunError,
1586
+ createServer,
1587
+ serveNode,
1588
+ toUIMessageStream,
1589
+ toUIMessageStreamResponse
1590
+ };