@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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/dist-VXER5V4E.js +1028 -0
- package/dist/index.cjs +2669 -0
- package/dist/index.d.cts +1068 -0
- package/dist/index.d.ts +1068 -0
- package/dist/index.js +1590 -0
- package/package.json +63 -0
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
|
+
};
|