elsium-ai 0.2.2 → 0.3.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 CHANGED
@@ -491,7 +491,16 @@ function envBool(name, fallback) {
491
491
  const raw = getEnvVar(name);
492
492
  if (raw !== undefined) {
493
493
  const normalized = raw.toLowerCase();
494
- return normalized === "true" || normalized === "1" || normalized === "yes";
494
+ if (normalized === "true" || normalized === "1" || normalized === "yes")
495
+ return true;
496
+ if (normalized === "false" || normalized === "0" || normalized === "no")
497
+ return false;
498
+ throw new ElsiumError({
499
+ code: "CONFIG_ERROR",
500
+ message: `Environment variable ${name} has unrecognized boolean value: ${raw}. Expected one of: true, false, 1, 0, yes, no`,
501
+ retryable: false,
502
+ metadata: { variable: name, value: raw }
503
+ });
495
504
  }
496
505
  if (fallback !== undefined)
497
506
  return fallback;
@@ -502,6 +511,243 @@ function envBool(name, fallback) {
502
511
  metadata: { variable: name }
503
512
  });
504
513
  }
514
+ // ../core/src/schema.ts
515
+ var log = createLogger();
516
+ function zodDefKind(def) {
517
+ return typeof def.type === "string" ? def.type : def.typeName;
518
+ }
519
+ function zodObjectToJsonSchema(schema, convert) {
520
+ const shape = typeof schema.shape === "function" ? schema.shape() : schema.shape;
521
+ const properties = {};
522
+ const required = [];
523
+ for (const [key, value] of Object.entries(shape)) {
524
+ const fieldSchema = value;
525
+ properties[key] = convert(fieldSchema);
526
+ const fieldDef = fieldSchema._def;
527
+ const fieldKind = zodDefKind(fieldDef);
528
+ if (fieldKind !== "optional" && fieldKind !== "ZodOptional" && fieldKind !== "default" && fieldKind !== "ZodDefault") {
529
+ required.push(key);
530
+ }
531
+ if (fieldDef.description) {
532
+ properties[key].description = fieldDef.description;
533
+ }
534
+ }
535
+ return { type: "object", properties, required };
536
+ }
537
+ function zodToJsonSchema(schema) {
538
+ if (!("_def" in schema))
539
+ return { type: "object" };
540
+ const def = schema._def;
541
+ const kind = zodDefKind(def);
542
+ switch (kind) {
543
+ case "object":
544
+ case "ZodObject":
545
+ return zodObjectToJsonSchema(def, zodToJsonSchema);
546
+ case "string":
547
+ case "ZodString":
548
+ return { type: "string" };
549
+ case "number":
550
+ case "ZodNumber":
551
+ return { type: "number" };
552
+ case "boolean":
553
+ case "ZodBoolean":
554
+ return { type: "boolean" };
555
+ case "array":
556
+ case "ZodArray":
557
+ return {
558
+ type: "array",
559
+ items: zodToJsonSchema(def.element ?? def.type)
560
+ };
561
+ case "enum":
562
+ case "ZodEnum": {
563
+ const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
564
+ return { type: "string", enum: values };
565
+ }
566
+ case "optional":
567
+ case "ZodOptional":
568
+ return zodToJsonSchema(def.innerType);
569
+ case "default":
570
+ case "ZodDefault":
571
+ return zodToJsonSchema(def.innerType);
572
+ case "nullable":
573
+ case "ZodNullable": {
574
+ const inner = zodToJsonSchema(def.innerType);
575
+ return { ...inner, nullable: true };
576
+ }
577
+ case "ZodLiteral":
578
+ return { type: typeof def.value, const: def.value };
579
+ case "ZodUnion": {
580
+ const options = def.options.map(zodToJsonSchema);
581
+ return { anyOf: options };
582
+ }
583
+ case "ZodRecord":
584
+ return {
585
+ type: "object",
586
+ additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : { type: "string" }
587
+ };
588
+ case "ZodTuple": {
589
+ const items = (def.items ?? []).map(zodToJsonSchema);
590
+ return { type: "array", prefixItems: items, minItems: items.length, maxItems: items.length };
591
+ }
592
+ case "ZodDate":
593
+ return { type: "string", format: "date-time" };
594
+ default:
595
+ log.warn(`zodToJsonSchema: unsupported type ${kind}, defaulting to string`);
596
+ return { type: "string" };
597
+ }
598
+ }
599
+ // ../core/src/registry.ts
600
+ var log2 = createLogger();
601
+ var BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
602
+ function createRegistry(label) {
603
+ const entries = new Map;
604
+ return {
605
+ register(name, factory) {
606
+ if (BLOCKED_KEYS.has(name)) {
607
+ log2.warn(`Registry(${label}): rejected blocked key "${name}"`);
608
+ return;
609
+ }
610
+ entries.set(name, factory);
611
+ log2.debug(`Registry(${label}): registered "${name}"`);
612
+ },
613
+ get(name) {
614
+ if (BLOCKED_KEYS.has(name))
615
+ return;
616
+ return entries.get(name);
617
+ },
618
+ list() {
619
+ return Array.from(entries.keys());
620
+ },
621
+ has(name) {
622
+ if (BLOCKED_KEYS.has(name))
623
+ return false;
624
+ return entries.has(name);
625
+ },
626
+ unregister(name) {
627
+ if (BLOCKED_KEYS.has(name))
628
+ return false;
629
+ const deleted = entries.delete(name);
630
+ if (deleted) {
631
+ log2.debug(`Registry(${label}): unregistered "${name}"`);
632
+ }
633
+ return deleted;
634
+ }
635
+ };
636
+ }
637
+ // ../core/src/tokens.ts
638
+ var MODEL_RATIOS = {
639
+ cl100k_base: 4,
640
+ "gpt-4o": 4,
641
+ "gpt-4o-mini": 4,
642
+ o1: 4,
643
+ "o1-mini": 4,
644
+ "o3-mini": 4,
645
+ "claude-opus-4-6": 3.5,
646
+ "claude-sonnet-4-6": 3.5,
647
+ "claude-haiku-4-5-20251001": 3.5,
648
+ "gemini-2.0-flash": 4,
649
+ "gemini-2.0-flash-lite": 4,
650
+ "gemini-2.5-pro-preview-05-06": 4,
651
+ "gemini-2.5-flash-preview-04-17": 4
652
+ };
653
+ function getRatio(model) {
654
+ if (!model)
655
+ return 4;
656
+ if (MODEL_RATIOS[model])
657
+ return MODEL_RATIOS[model];
658
+ if (model.startsWith("claude"))
659
+ return 3.5;
660
+ if (model.startsWith("gemini"))
661
+ return 4;
662
+ if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3"))
663
+ return 4;
664
+ return 4;
665
+ }
666
+ function extractMessageText(msg) {
667
+ if (typeof msg.content === "string")
668
+ return msg.content;
669
+ return msg.content.filter((p) => p.type === "text" && ("text" in p)).map((p) => p.text).join("");
670
+ }
671
+ function countTokens(text, model) {
672
+ const ratio = getRatio(model);
673
+ return Math.ceil(text.length / ratio) + 4;
674
+ }
675
+ function createContextManager(config) {
676
+ const { maxTokens, strategy, reserveTokens = 0 } = config;
677
+ const budget = maxTokens - reserveTokens;
678
+ function estimateMessageTokens(msg) {
679
+ return countTokens(extractMessageText(msg)) + 4;
680
+ }
681
+ function estimateTokens(messages) {
682
+ return messages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
683
+ }
684
+ async function fitTruncate(messages, system) {
685
+ let available = budget;
686
+ if (system)
687
+ available -= countTokens(system);
688
+ const result = [];
689
+ for (let i = messages.length - 1;i >= 0; i--) {
690
+ const tokens = estimateMessageTokens(messages[i]);
691
+ if (available - tokens < 0 && result.length > 0)
692
+ break;
693
+ available -= tokens;
694
+ result.unshift(messages[i]);
695
+ }
696
+ return result;
697
+ }
698
+ async function fitSummarize(messages, system) {
699
+ if (!config.summarizer)
700
+ return fitTruncate(messages, system);
701
+ let available = budget;
702
+ if (system)
703
+ available -= countTokens(system);
704
+ const total = estimateTokens(messages);
705
+ if (total <= available)
706
+ return messages;
707
+ const keepCount = Math.max(1, Math.floor(messages.length / 3));
708
+ const toSummarize = messages.slice(0, messages.length - keepCount);
709
+ const toKeep = messages.slice(messages.length - keepCount);
710
+ const summary = await config.summarizer(toSummarize);
711
+ const summaryMsg = {
712
+ role: "system",
713
+ content: `Previous conversation summary: ${summary}`
714
+ };
715
+ return [summaryMsg, ...toKeep];
716
+ }
717
+ async function fitSlidingWindow(messages, system) {
718
+ let available = budget;
719
+ if (system)
720
+ available -= countTokens(system);
721
+ const result = [];
722
+ for (let i = messages.length - 1;i >= 0; i--) {
723
+ const tokens = estimateMessageTokens(messages[i]);
724
+ if (available - tokens < 0 && result.length > 0)
725
+ break;
726
+ available -= tokens;
727
+ result.unshift(messages[i]);
728
+ }
729
+ return result;
730
+ }
731
+ return {
732
+ estimateTokens,
733
+ async fit(messages, system) {
734
+ const total = estimateTokens(messages);
735
+ let available = budget;
736
+ if (system)
737
+ available -= countTokens(system);
738
+ if (total <= available)
739
+ return messages;
740
+ switch (strategy) {
741
+ case "truncate":
742
+ return fitTruncate(messages, system);
743
+ case "summarize":
744
+ return fitSummarize(messages, system);
745
+ case "sliding-window":
746
+ return fitSlidingWindow(messages, system);
747
+ }
748
+ }
749
+ };
750
+ }
505
751
  // ../core/src/circuit-breaker.ts
506
752
  function defaultShouldCount(error) {
507
753
  if (error && typeof error === "object" && "retryable" in error) {
@@ -630,6 +876,101 @@ function createCircuitBreaker(config) {
630
876
  }
631
877
  };
632
878
  }
879
+ // ../core/src/shutdown.ts
880
+ function createShutdownManager(config) {
881
+ const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
882
+ const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
883
+ if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
884
+ throw new ElsiumError({
885
+ code: "CONFIG_ERROR",
886
+ message: "drainTimeoutMs must be >= 0 and finite",
887
+ retryable: false
888
+ });
889
+ }
890
+ let shuttingDown = false;
891
+ let inFlightCount = 0;
892
+ let drainResolve = null;
893
+ let shutdownPromise = null;
894
+ const signalHandlers = [];
895
+ function checkDrained() {
896
+ if (inFlightCount === 0 && drainResolve) {
897
+ drainResolve();
898
+ drainResolve = null;
899
+ }
900
+ }
901
+ async function shutdown() {
902
+ if (shutdownPromise)
903
+ return shutdownPromise;
904
+ shuttingDown = true;
905
+ shutdownPromise = (async () => {
906
+ config?.onDrainStart?.();
907
+ if (inFlightCount === 0) {
908
+ config?.onDrainComplete?.();
909
+ return;
910
+ }
911
+ const drainPromise = new Promise((resolve) => {
912
+ drainResolve = resolve;
913
+ });
914
+ let drainTimer;
915
+ const timeoutPromise = new Promise((resolve) => {
916
+ drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
917
+ });
918
+ const result = await Promise.race([
919
+ drainPromise.then(() => "drained"),
920
+ timeoutPromise
921
+ ]);
922
+ if (drainTimer !== undefined)
923
+ clearTimeout(drainTimer);
924
+ if (result === "timeout") {
925
+ config?.onForceShutdown?.();
926
+ } else {
927
+ config?.onDrainComplete?.();
928
+ }
929
+ })();
930
+ return shutdownPromise;
931
+ }
932
+ const manager = {
933
+ get inFlight() {
934
+ return inFlightCount;
935
+ },
936
+ get isShuttingDown() {
937
+ return shuttingDown;
938
+ },
939
+ async trackOperation(fn) {
940
+ if (shuttingDown) {
941
+ throw new ElsiumError({
942
+ code: "VALIDATION_ERROR",
943
+ message: "Server is shutting down, not accepting new operations",
944
+ retryable: true
945
+ });
946
+ }
947
+ inFlightCount++;
948
+ try {
949
+ return await fn();
950
+ } finally {
951
+ inFlightCount--;
952
+ checkDrained();
953
+ }
954
+ },
955
+ shutdown,
956
+ dispose() {
957
+ for (const { signal, handler } of signalHandlers) {
958
+ process.removeListener(signal, handler);
959
+ }
960
+ signalHandlers.length = 0;
961
+ }
962
+ };
963
+ if (typeof process !== "undefined" && process.on) {
964
+ for (const signal of signals) {
965
+ const handler = () => {
966
+ manager.shutdown();
967
+ };
968
+ signalHandlers.push({ signal, handler });
969
+ process.on(signal, handler);
970
+ }
971
+ }
972
+ return manager;
973
+ }
633
974
  // ../gateway/src/provider.ts
634
975
  var providerRegistry = new Map;
635
976
  var metadataRegistry = new Map;
@@ -659,16 +1000,16 @@ function composeMiddleware(middlewares) {
659
1000
  };
660
1001
  }
661
1002
  function loggingMiddleware(logger) {
662
- const log = logger ?? createLogger({ level: "info" });
1003
+ const log3 = logger ?? createLogger({ level: "info" });
663
1004
  return async (ctx, next) => {
664
- log.info("LLM request", {
1005
+ log3.info("LLM request", {
665
1006
  provider: ctx.provider,
666
1007
  model: ctx.model,
667
1008
  traceId: ctx.traceId,
668
1009
  messageCount: ctx.request.messages.length
669
1010
  });
670
1011
  const response = await next(ctx);
671
- log.info("LLM response", {
1012
+ log3.info("LLM response", {
672
1013
  provider: ctx.provider,
673
1014
  model: ctx.model,
674
1015
  traceId: ctx.traceId,
@@ -804,7 +1145,7 @@ function xrayMiddleware(options = {}) {
804
1145
  }
805
1146
 
806
1147
  // ../gateway/src/pricing.ts
807
- var log = createLogger();
1148
+ var log3 = createLogger();
808
1149
  var PRICING = {
809
1150
  "claude-opus-4-6": { inputPerMillion: 15, outputPerMillion: 75 },
810
1151
  "claude-sonnet-4-6": { inputPerMillion: 3, outputPerMillion: 15 },
@@ -842,7 +1183,7 @@ function resolveModelName(model) {
842
1183
  function calculateCost(model, usage) {
843
1184
  const pricing = PRICING[resolveModelName(model)];
844
1185
  if (!pricing) {
845
- log.warn(`Unknown model "${model}" — cost will be reported as $0. Register pricing with registerPricing().`);
1186
+ log3.warn(`Unknown model "${model}" — cost will be reported as $0. Register pricing with registerPricing().`);
846
1187
  return {
847
1188
  inputCost: 0,
848
1189
  outputCost: 0,
@@ -936,15 +1277,33 @@ function createAnthropicProvider(config) {
936
1277
  if (part.type === "text")
937
1278
  return { type: "text", text: part.text };
938
1279
  if (part.type === "image" && part.source?.type === "base64") {
1280
+ const src = part.source;
939
1281
  return {
940
1282
  type: "image",
941
1283
  source: {
942
1284
  type: "base64",
943
- media_type: part.source.mediaType,
944
- data: part.source.data
1285
+ media_type: src.mediaType,
1286
+ data: src.data
945
1287
  }
946
1288
  };
947
1289
  }
1290
+ if (part.type === "document" && part.source) {
1291
+ if (part.source.type === "base64") {
1292
+ const src = part.source;
1293
+ return {
1294
+ type: "document",
1295
+ source: {
1296
+ type: "base64",
1297
+ media_type: src.mediaType,
1298
+ data: src.data
1299
+ }
1300
+ };
1301
+ }
1302
+ return { type: "text", text: "[document: url source not supported by Anthropic]" };
1303
+ }
1304
+ if (part.type === "audio") {
1305
+ return { type: "text", text: "[audio content not supported by this provider]" };
1306
+ }
948
1307
  return { type: "text", text: "[unsupported content]" };
949
1308
  }
950
1309
  function formatMultipartContent(msg, role) {
@@ -1053,6 +1412,16 @@ function createAnthropicProvider(config) {
1053
1412
  const tools = formatTools(req.tools);
1054
1413
  if (tools)
1055
1414
  body.tools = tools;
1415
+ if (req.schema) {
1416
+ const jsonSchema = zodToJsonSchema(req.schema);
1417
+ const structuredTool = {
1418
+ name: "_structured_output",
1419
+ description: "Return structured output matching the required schema",
1420
+ input_schema: jsonSchema
1421
+ };
1422
+ body.tools = [...tools ?? [], structuredTool];
1423
+ body.tool_choice = { type: "tool", name: "_structured_output" };
1424
+ }
1056
1425
  const startTime = performance.now();
1057
1426
  const raw = await retry(async () => {
1058
1427
  const controller = new AbortController;
@@ -1262,7 +1631,31 @@ function createGoogleProvider(config) {
1262
1631
  return { role, parts };
1263
1632
  }
1264
1633
  function formatGeminiMultipartContent(msg, role) {
1265
- const parts = msg.content.filter((p) => p.type === "text").map((p) => ({ text: p.text }));
1634
+ const parts = [];
1635
+ for (const p of msg.content) {
1636
+ if (p.type === "text") {
1637
+ parts.push({ text: p.text });
1638
+ } else if (p.type === "image") {
1639
+ const img = p;
1640
+ if (img.source.type === "base64") {
1641
+ parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
1642
+ } else {
1643
+ parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
1644
+ }
1645
+ } else if (p.type === "audio" || p.type === "document") {
1646
+ const media = p;
1647
+ if (media.source.type === "base64") {
1648
+ parts.push({
1649
+ inlineData: { mimeType: media.source.mediaType, data: media.source.data }
1650
+ });
1651
+ } else {
1652
+ const urlSource = media.source;
1653
+ parts.push({
1654
+ fileData: { mimeType: "application/octet-stream", fileUri: urlSource.url }
1655
+ });
1656
+ }
1657
+ }
1658
+ }
1266
1659
  return { role, parts };
1267
1660
  }
1268
1661
  function formatMessages(messages) {
@@ -1359,6 +1752,10 @@ function createGoogleProvider(config) {
1359
1752
  config2.topP = req.topP;
1360
1753
  if (req.stopSequences?.length)
1361
1754
  config2.stopSequences = req.stopSequences;
1755
+ if (req.schema) {
1756
+ config2.responseMimeType = "application/json";
1757
+ config2.responseSchema = zodToJsonSchema(req.schema);
1758
+ }
1362
1759
  return config2;
1363
1760
  }
1364
1761
  function buildRequestBody(req) {
@@ -1437,7 +1834,8 @@ async function handleGoogleErrorResponse(response) {
1437
1834
  throw ElsiumError.authError("google");
1438
1835
  }
1439
1836
  if (response.status === 429) {
1440
- throw ElsiumError.rateLimit("google");
1837
+ const retryAfter = response.headers.get("retry-after");
1838
+ throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
1441
1839
  }
1442
1840
  throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
1443
1841
  provider: "google",
@@ -1623,6 +2021,43 @@ function createOpenAIProvider(config) {
1623
2021
  }
1624
2022
  return openaiMsg;
1625
2023
  }
2024
+ function formatUserContent(msg) {
2025
+ if (typeof msg.content === "string")
2026
+ return msg.content;
2027
+ const parts = [];
2028
+ for (const part of msg.content) {
2029
+ if (part.type === "text") {
2030
+ parts.push({ type: "text", text: part.text });
2031
+ } else if (part.type === "image") {
2032
+ if (part.source.type === "base64") {
2033
+ const url = `data:${part.source.mediaType};base64,${part.source.data}`;
2034
+ parts.push({ type: "image_url", image_url: { url } });
2035
+ } else {
2036
+ parts.push({ type: "image_url", image_url: { url: part.source.url } });
2037
+ }
2038
+ } else if (part.type === "audio") {
2039
+ if (part.source.type === "base64") {
2040
+ const format = part.source.mediaType.split("/")[1] ?? "wav";
2041
+ parts.push({
2042
+ type: "input_audio",
2043
+ input_audio: { data: part.source.data, format }
2044
+ });
2045
+ } else {
2046
+ parts.push({ type: "text", text: "[audio: url source requires file upload]" });
2047
+ }
2048
+ } else if (part.type === "document") {
2049
+ if (part.source.type === "base64") {
2050
+ parts.push({
2051
+ type: "text",
2052
+ text: `[document: ${part.source.mediaType} content attached as base64]`
2053
+ });
2054
+ } else {
2055
+ parts.push({ type: "text", text: `[document: ${part.source.url}]` });
2056
+ }
2057
+ }
2058
+ }
2059
+ return parts;
2060
+ }
1626
2061
  function formatMessages(messages) {
1627
2062
  const formatted = [];
1628
2063
  for (const msg of messages) {
@@ -1638,7 +2073,7 @@ function createOpenAIProvider(config) {
1638
2073
  formatted.push(formatAssistantMessage(msg));
1639
2074
  continue;
1640
2075
  }
1641
- formatted.push({ role: "user", content: extractTextContent(msg) });
2076
+ formatted.push({ role: "user", content: formatUserContent(msg) });
1642
2077
  }
1643
2078
  return formatted;
1644
2079
  }
@@ -1715,6 +2150,17 @@ function createOpenAIProvider(config) {
1715
2150
  const tools = formatTools(req.tools);
1716
2151
  if (tools)
1717
2152
  body.tools = tools;
2153
+ if (req.schema) {
2154
+ const jsonSchema = zodToJsonSchema(req.schema);
2155
+ body.response_format = {
2156
+ type: "json_schema",
2157
+ json_schema: {
2158
+ name: "structured_output",
2159
+ strict: true,
2160
+ schema: jsonSchema
2161
+ }
2162
+ };
2163
+ }
1718
2164
  const startTime = performance.now();
1719
2165
  const raw = await retry(async () => {
1720
2166
  const controller = new AbortController;
@@ -1877,7 +2323,7 @@ var PROVIDER_FACTORIES = {
1877
2323
  function registerProviderFactory(name, factory) {
1878
2324
  PROVIDER_FACTORIES[name] = factory;
1879
2325
  }
1880
- function gateway(config) {
2326
+ function validateGatewayConfig(config) {
1881
2327
  const factory = PROVIDER_FACTORIES[config.provider];
1882
2328
  if (!factory) {
1883
2329
  throw new ElsiumError({
@@ -1886,21 +2332,92 @@ function gateway(config) {
1886
2332
  retryable: false
1887
2333
  });
1888
2334
  }
2335
+ if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
2336
+ throw new ElsiumError({
2337
+ code: "CONFIG_ERROR",
2338
+ message: "apiKey must be a non-empty string",
2339
+ retryable: false
2340
+ });
2341
+ }
2342
+ if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
2343
+ throw new ElsiumError({
2344
+ code: "CONFIG_ERROR",
2345
+ message: "timeout must be a positive finite number",
2346
+ retryable: false
2347
+ });
2348
+ }
2349
+ if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
2350
+ throw new ElsiumError({
2351
+ code: "CONFIG_ERROR",
2352
+ message: "maxRetries must be a non-negative finite integer",
2353
+ retryable: false
2354
+ });
2355
+ }
2356
+ return factory;
2357
+ }
2358
+ function autoRegisterProvider(provider) {
2359
+ if (!provider.metadata)
2360
+ return;
2361
+ registerProviderMetadata(provider.name, provider.metadata);
2362
+ if (!provider.metadata.pricing)
2363
+ return;
2364
+ for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
2365
+ registerPricing(model, pricing);
2366
+ }
2367
+ }
2368
+ function validateRequestLimits(request, maxMessages, maxInputTokens) {
2369
+ if (request.messages.length > maxMessages) {
2370
+ throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
2371
+ }
2372
+ let estimatedTokens = 0;
2373
+ for (const msg of request.messages) {
2374
+ const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
2375
+ estimatedTokens += Math.ceil(text.length / 4);
2376
+ }
2377
+ if (estimatedTokens > maxInputTokens) {
2378
+ throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
2379
+ }
2380
+ }
2381
+ function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
2382
+ return {
2383
+ request: req,
2384
+ provider: providerName,
2385
+ model: req.model ?? defaultModel,
2386
+ traceId: generateTraceId(),
2387
+ startTime: performance.now(),
2388
+ metadata
2389
+ };
2390
+ }
2391
+ async function accumulateStreamEvents(stream, emit) {
2392
+ let textContent = "";
2393
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
2394
+ let stopReason = "end_turn";
2395
+ let id = "";
2396
+ for await (const event of stream) {
2397
+ emit(event);
2398
+ if (event.type === "text_delta") {
2399
+ textContent += event.text;
2400
+ } else if (event.type === "message_end") {
2401
+ usage = event.usage;
2402
+ stopReason = event.stopReason;
2403
+ } else if (event.type === "message_start") {
2404
+ id = event.id;
2405
+ }
2406
+ }
2407
+ return { textContent, usage, stopReason, id };
2408
+ }
2409
+ function gateway(config) {
2410
+ const factory = validateGatewayConfig(config);
1889
2411
  const provider = factory({
1890
2412
  apiKey: config.apiKey,
1891
2413
  baseUrl: config.baseUrl,
1892
2414
  timeout: config.timeout,
1893
2415
  maxRetries: config.maxRetries
1894
2416
  });
1895
- if (provider.metadata) {
1896
- registerProviderMetadata(provider.name, provider.metadata);
1897
- if (provider.metadata.pricing) {
1898
- for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
1899
- registerPricing(model, pricing);
1900
- }
1901
- }
1902
- }
2417
+ autoRegisterProvider(provider);
1903
2418
  const defaultModel = config.model ?? provider.defaultModel;
2419
+ const maxMessages = config.maxMessages ?? 1000;
2420
+ const maxInputTokens = config.maxInputTokens ?? 1e6;
1904
2421
  let xrayStore = null;
1905
2422
  const allMiddleware = [...config.middleware ?? []];
1906
2423
  if (config.xray) {
@@ -1915,14 +2432,7 @@ function gateway(config) {
1915
2432
  if (!composedMiddleware) {
1916
2433
  return provider.complete(req);
1917
2434
  }
1918
- const ctx = {
1919
- request: req,
1920
- provider: provider.name,
1921
- model: req.model ?? defaultModel,
1922
- traceId: generateTraceId(),
1923
- startTime: performance.now(),
1924
- metadata: request.metadata ?? {}
1925
- };
2435
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1926
2436
  return composedMiddleware(ctx, async (c) => provider.complete(c.request));
1927
2437
  }
1928
2438
  return {
@@ -1934,34 +2444,27 @@ function gateway(config) {
1934
2444
  return xrayStore?.callHistory(limit) ?? [];
1935
2445
  },
1936
2446
  async complete(request) {
2447
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1937
2448
  return executeWithMiddleware(request);
1938
2449
  },
1939
2450
  stream(request) {
2451
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1940
2452
  const req = { ...request, model: request.model ?? defaultModel };
1941
2453
  if (composedMiddleware) {
1942
- const ctx = {
1943
- request: req,
1944
- provider: provider.name,
1945
- model: req.model ?? defaultModel,
1946
- traceId: generateTraceId(),
1947
- startTime: performance.now(),
1948
- metadata: request.metadata ?? {}
1949
- };
2454
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1950
2455
  return createStream(async (emit) => {
1951
2456
  await composedMiddleware(ctx, async (c) => {
1952
- const stream = provider.stream(c.request);
1953
- for await (const event of stream) {
1954
- emit(event);
1955
- }
2457
+ const result = await accumulateStreamEvents(provider.stream(c.request), emit);
2458
+ const latencyMs = Math.round(performance.now() - ctx.startTime);
1956
2459
  return {
1957
- id: "",
1958
- message: { role: "assistant", content: "" },
1959
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1960
- cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: "USD" },
2460
+ id: result.id,
2461
+ message: { role: "assistant", content: result.textContent },
2462
+ usage: result.usage,
2463
+ cost: calculateCost(c.model, result.usage),
1961
2464
  model: c.model,
1962
2465
  provider: provider.name,
1963
- stopReason: "end_turn",
1964
- latencyMs: 0,
2466
+ stopReason: result.stopReason,
2467
+ latencyMs,
1965
2468
  traceId: ctx.traceId
1966
2469
  };
1967
2470
  });
@@ -1971,107 +2474,46 @@ function gateway(config) {
1971
2474
  },
1972
2475
  async generate(request) {
1973
2476
  const { schema, ...rest } = request;
1974
- const jsonSchema = schemaToJsonSchema(schema);
1975
- const systemPrompt = [
1976
- rest.system ?? "",
1977
- "You MUST respond with valid JSON matching this schema:",
1978
- JSON.stringify(jsonSchema, null, 2),
1979
- "Respond ONLY with the JSON object, no markdown or explanation."
1980
- ].filter(Boolean).join(`
1981
-
1982
- `);
2477
+ const jsonSchema = zodToJsonSchema(schema);
1983
2478
  const response = await executeWithMiddleware({
1984
2479
  ...rest,
1985
- system: systemPrompt
2480
+ schema,
2481
+ system: [
2482
+ rest.system ?? "",
2483
+ "You MUST respond with valid JSON matching this schema:",
2484
+ JSON.stringify(jsonSchema, null, 2),
2485
+ "Respond ONLY with the JSON object, no markdown or explanation."
2486
+ ].filter(Boolean).join(`
2487
+
2488
+ `)
1986
2489
  });
1987
- const text = typeof response.message.content === "string" ? response.message.content : "";
1988
- const jsonMatch = text.match(/\{[\s\S]*\}/);
1989
- if (!jsonMatch) {
1990
- throw ElsiumError.validation("LLM response did not contain valid JSON", {
1991
- response: text
1992
- });
2490
+ let parsed;
2491
+ if (response.stopReason === "tool_use" && response.message.toolCalls?.length) {
2492
+ const structuredCall = response.message.toolCalls.find((tc) => tc.name === "_structured_output");
2493
+ if (structuredCall) {
2494
+ parsed = structuredCall.arguments;
2495
+ }
2496
+ }
2497
+ if (parsed === undefined) {
2498
+ const text = typeof response.message.content === "string" ? response.message.content : "";
2499
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
2500
+ if (!jsonMatch) {
2501
+ throw ElsiumError.validation("LLM response did not contain valid JSON", {
2502
+ response: text
2503
+ });
2504
+ }
2505
+ parsed = JSON.parse(jsonMatch[0]);
1993
2506
  }
1994
- const parsed = JSON.parse(jsonMatch[0]);
1995
2507
  const result = schema.safeParse(parsed);
1996
2508
  if (!result.success) {
1997
2509
  throw ElsiumError.validation("LLM response did not match schema", {
1998
- errors: result.error.issues,
1999
- response: text
2510
+ errors: result.error.issues
2000
2511
  });
2001
2512
  }
2002
2513
  return { data: result.data, response };
2003
2514
  }
2004
2515
  };
2005
2516
  }
2006
- function schemaToJsonSchema(schema) {
2007
- try {
2008
- if ("_def" in schema) {
2009
- const def = schema._def;
2010
- const result = convertZodDef(def);
2011
- if (result)
2012
- return result;
2013
- }
2014
- } catch {}
2015
- return { type: "string" };
2016
- }
2017
- function zodDefKind(def) {
2018
- return typeof def.type === "string" ? def.type : def.typeName;
2019
- }
2020
- function convertZodDef(def) {
2021
- const kind = zodDefKind(def);
2022
- switch (kind) {
2023
- case "object":
2024
- case "ZodObject":
2025
- return convertZodObject(def);
2026
- case "string":
2027
- case "ZodString":
2028
- return { type: "string" };
2029
- case "number":
2030
- case "ZodNumber":
2031
- return { type: "number" };
2032
- case "boolean":
2033
- case "ZodBoolean":
2034
- return { type: "boolean" };
2035
- case "array":
2036
- case "ZodArray":
2037
- return convertZodArray(def);
2038
- case "enum":
2039
- case "ZodEnum": {
2040
- const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
2041
- return { type: "string", enum: values };
2042
- }
2043
- case "optional":
2044
- case "ZodOptional":
2045
- return convertZodOptional(def);
2046
- default:
2047
- return null;
2048
- }
2049
- }
2050
- function convertZodObject(def) {
2051
- if (!def.shape)
2052
- return null;
2053
- const shape = typeof def.shape === "function" ? def.shape() : def.shape;
2054
- const properties = {};
2055
- const required = [];
2056
- for (const [key, value] of Object.entries(shape)) {
2057
- properties[key] = schemaToJsonSchema(value);
2058
- const valDef = value._def;
2059
- const valKind = zodDefKind(valDef);
2060
- if (valKind !== "optional" && valKind !== "ZodOptional") {
2061
- required.push(key);
2062
- }
2063
- }
2064
- return { type: "object", properties, required };
2065
- }
2066
- function convertZodArray(def) {
2067
- return {
2068
- type: "array",
2069
- items: schemaToJsonSchema(def.element ?? def.type)
2070
- };
2071
- }
2072
- function convertZodOptional(def) {
2073
- return schemaToJsonSchema(def.innerType ?? def.innerType);
2074
- }
2075
2517
  // ../gateway/src/security.ts
2076
2518
  var INJECTION_PATTERNS = [
2077
2519
  {
@@ -2245,75 +2687,396 @@ function redactSecrets(text, piiTypes) {
2245
2687
  redacted = piiResult.redacted;
2246
2688
  found.push(...piiResult.found);
2247
2689
  }
2248
- return { redacted, found };
2249
- }
2250
- function checkBlockedPatterns(text, patterns) {
2690
+ return { redacted, found };
2691
+ }
2692
+ function checkBlockedPatterns(text, patterns) {
2693
+ const violations = [];
2694
+ for (const pattern of patterns) {
2695
+ pattern.lastIndex = 0;
2696
+ if (pattern.test(text)) {
2697
+ violations.push({
2698
+ type: "blocked_pattern",
2699
+ detail: `Blocked pattern matched: ${pattern.source}`,
2700
+ severity: "medium"
2701
+ });
2702
+ }
2703
+ }
2704
+ return violations;
2705
+ }
2706
+ function scanMessageForViolations(text, config) {
2707
+ const violations = [];
2708
+ if (config.promptInjection !== false) {
2709
+ violations.push(...detectPromptInjection(text));
2710
+ }
2711
+ if (config.jailbreakDetection) {
2712
+ violations.push(...detectJailbreak(text));
2713
+ }
2714
+ if (config.blockedPatterns?.length) {
2715
+ violations.push(...checkBlockedPatterns(text, config.blockedPatterns));
2716
+ }
2717
+ return violations;
2718
+ }
2719
+ function reportAndThrow(violations, config) {
2720
+ for (const v of violations) {
2721
+ config.onViolation?.(v);
2722
+ }
2723
+ throw ElsiumError.validation(`Security violation detected: ${violations.map((v) => v.detail).join("; ")}`);
2724
+ }
2725
+ function redactResponseSecrets(response, config) {
2726
+ const responseText = extractText(response.message.content);
2727
+ if (!responseText)
2728
+ return response;
2729
+ const { redacted, found } = redactSecrets(responseText, config.piiTypes);
2730
+ if (found.length === 0)
2731
+ return response;
2732
+ for (const v of found) {
2733
+ config.onViolation?.(v);
2734
+ }
2735
+ return {
2736
+ ...response,
2737
+ message: {
2738
+ ...response.message,
2739
+ content: redacted
2740
+ }
2741
+ };
2742
+ }
2743
+ function securityMiddleware(config) {
2744
+ return async (ctx, next) => {
2745
+ if (ctx.request.system) {
2746
+ const sysViolations = scanMessageForViolations(ctx.request.system, config);
2747
+ if (sysViolations.length > 0) {
2748
+ reportAndThrow(sysViolations, config);
2749
+ }
2750
+ }
2751
+ for (const message of ctx.request.messages) {
2752
+ const text = extractText(message.content);
2753
+ if (!text)
2754
+ continue;
2755
+ const violations = scanMessageForViolations(text, config);
2756
+ if (violations.length > 0) {
2757
+ reportAndThrow(violations, config);
2758
+ }
2759
+ }
2760
+ const response = await next(ctx);
2761
+ if (config.secretRedaction !== false) {
2762
+ return redactResponseSecrets(response, config);
2763
+ }
2764
+ return response;
2765
+ };
2766
+ }
2767
+ // ../gateway/src/cache.ts
2768
+ import { createHash } from "node:crypto";
2769
+ var log4 = createLogger();
2770
+ function createInMemoryCache(maxSize = 1000) {
2771
+ const cache = new Map;
2772
+ function evict() {
2773
+ if (cache.size <= maxSize)
2774
+ return;
2775
+ const firstKey = cache.keys().next().value;
2776
+ if (firstKey !== undefined)
2777
+ cache.delete(firstKey);
2778
+ }
2779
+ return {
2780
+ async get(key) {
2781
+ const entry = cache.get(key);
2782
+ if (!entry)
2783
+ return null;
2784
+ if (Date.now() > entry.expiresAt) {
2785
+ cache.delete(key);
2786
+ return null;
2787
+ }
2788
+ cache.delete(key);
2789
+ cache.set(key, entry);
2790
+ return entry.value;
2791
+ },
2792
+ async set(key, value, ttlMs) {
2793
+ cache.set(key, { value, expiresAt: Date.now() + ttlMs });
2794
+ evict();
2795
+ },
2796
+ async delete(key) {
2797
+ cache.delete(key);
2798
+ },
2799
+ async clear() {
2800
+ cache.clear();
2801
+ }
2802
+ };
2803
+ }
2804
+ function defaultCacheKey(ctx) {
2805
+ const data = JSON.stringify({
2806
+ provider: ctx.provider,
2807
+ model: ctx.model,
2808
+ messages: ctx.request.messages,
2809
+ system: ctx.request.system,
2810
+ temperature: ctx.request.temperature
2811
+ });
2812
+ return createHash("sha256").update(data).digest("hex");
2813
+ }
2814
+ function defaultShouldCache(_ctx, response) {
2815
+ const temp = _ctx.request.temperature;
2816
+ if (temp !== undefined && temp !== 0)
2817
+ return false;
2818
+ return response.stopReason === "end_turn";
2819
+ }
2820
+ function cacheMiddleware(config) {
2821
+ const ttlMs = config?.ttlMs ?? 3600000;
2822
+ const adapter = config?.adapter ?? createInMemoryCache(config?.maxSize ?? 1000);
2823
+ const keyFn = config?.keyFn ?? defaultCacheKey;
2824
+ const shouldCache = config?.shouldCache ?? defaultShouldCache;
2825
+ let hits = 0;
2826
+ let misses = 0;
2827
+ const middleware = async (ctx, next) => {
2828
+ if (ctx.request.stream) {
2829
+ return next(ctx);
2830
+ }
2831
+ const key = keyFn(ctx);
2832
+ const cached = await adapter.get(key);
2833
+ if (cached) {
2834
+ hits++;
2835
+ log4.debug("Cache hit", { key: key.slice(0, 8), provider: ctx.provider });
2836
+ return cached;
2837
+ }
2838
+ misses++;
2839
+ const response = await next(ctx);
2840
+ if (shouldCache(ctx, response)) {
2841
+ await adapter.set(key, response, ttlMs);
2842
+ }
2843
+ return response;
2844
+ };
2845
+ return Object.assign(middleware, {
2846
+ adapter,
2847
+ stats() {
2848
+ const total = hits + misses;
2849
+ return {
2850
+ hits,
2851
+ misses,
2852
+ size: 0,
2853
+ hitRate: total > 0 ? hits / total : 0
2854
+ };
2855
+ }
2856
+ });
2857
+ }
2858
+ // ../gateway/src/output-guardrails.ts
2859
+ var log5 = createLogger();
2860
+ var PII_PATTERNS2 = [
2861
+ {
2862
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
2863
+ label: "email",
2864
+ replacement: "[REDACTED_EMAIL]"
2865
+ },
2866
+ {
2867
+ pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
2868
+ label: "phone",
2869
+ replacement: "[REDACTED_PHONE]"
2870
+ },
2871
+ {
2872
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
2873
+ label: "ssn",
2874
+ replacement: "[REDACTED_SSN]"
2875
+ },
2876
+ {
2877
+ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
2878
+ label: "credit_card",
2879
+ replacement: "[REDACTED_CC]"
2880
+ }
2881
+ ];
2882
+ var SECRET_PATTERNS2 = [
2883
+ {
2884
+ pattern: /\bsk-[A-Za-z0-9]{20,}\b/g,
2885
+ label: "api_key",
2886
+ replacement: "[REDACTED_API_KEY]"
2887
+ },
2888
+ {
2889
+ pattern: /\bpk-[A-Za-z0-9]{20,}\b/g,
2890
+ label: "api_key",
2891
+ replacement: "[REDACTED_API_KEY]"
2892
+ },
2893
+ {
2894
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g,
2895
+ label: "aws_key",
2896
+ replacement: "[REDACTED_AWS_KEY]"
2897
+ }
2898
+ ];
2899
+ function detectPII(text) {
2251
2900
  const violations = [];
2252
- for (const pattern of patterns) {
2253
- pattern.lastIndex = 0;
2254
- if (pattern.test(text)) {
2901
+ const normalized = text.normalize("NFKC");
2902
+ for (const { pattern, label } of [...PII_PATTERNS2, ...SECRET_PATTERNS2]) {
2903
+ const regex = new RegExp(pattern.source, pattern.flags);
2904
+ if (regex.test(normalized)) {
2255
2905
  violations.push({
2256
- type: "blocked_pattern",
2257
- detail: `Blocked pattern matched: ${pattern.source}`,
2258
- severity: "medium"
2906
+ type: "pii",
2907
+ detail: `Detected ${label} in output`,
2908
+ pattern: label
2259
2909
  });
2260
2910
  }
2261
2911
  }
2262
2912
  return violations;
2263
2913
  }
2264
- function scanMessageForViolations(text, config) {
2265
- const violations = [];
2266
- if (config.promptInjection !== false) {
2267
- violations.push(...detectPromptInjection(text));
2268
- }
2269
- if (config.jailbreakDetection) {
2270
- violations.push(...detectJailbreak(text));
2914
+ function redactPII(text) {
2915
+ let result = text;
2916
+ for (const { pattern, replacement } of [...PII_PATTERNS2, ...SECRET_PATTERNS2]) {
2917
+ const regex = new RegExp(pattern.source, pattern.flags);
2918
+ result = result.replace(regex, replacement);
2271
2919
  }
2272
- if (config.blockedPatterns?.length) {
2273
- violations.push(...checkBlockedPatterns(text, config.blockedPatterns));
2920
+ return result;
2921
+ }
2922
+ function checkContentPolicy(text, policy) {
2923
+ const violations = [];
2924
+ if (policy.maxResponseLength && text.length > policy.maxResponseLength) {
2925
+ violations.push({
2926
+ type: "content_policy",
2927
+ detail: `Response length ${text.length} exceeds max ${policy.maxResponseLength}`
2928
+ });
2929
+ }
2930
+ if (policy.blockedPatterns) {
2931
+ for (const pattern of policy.blockedPatterns) {
2932
+ if (pattern.test(text)) {
2933
+ violations.push({
2934
+ type: "content_policy",
2935
+ detail: `Response matches blocked pattern: ${pattern.source}`,
2936
+ pattern: pattern.source
2937
+ });
2938
+ }
2939
+ }
2274
2940
  }
2275
2941
  return violations;
2276
2942
  }
2277
- function reportAndThrow(violations, config) {
2278
- for (const v of violations) {
2279
- config.onViolation?.(v);
2943
+ function checkCustomRules(text, rules) {
2944
+ const violations = [];
2945
+ for (const rule of rules) {
2946
+ if (rule.pattern.test(text)) {
2947
+ violations.push({
2948
+ type: "custom_rule",
2949
+ detail: rule.message ?? `Output matched custom rule: ${rule.name}`,
2950
+ pattern: rule.pattern.source
2951
+ });
2952
+ }
2280
2953
  }
2281
- throw ElsiumError.validation(`Security violation detected: ${violations.map((v) => v.detail).join("; ")}`);
2954
+ return violations;
2282
2955
  }
2283
- function redactResponseSecrets(response, config) {
2284
- const responseText = extractText(response.message.content);
2285
- if (!responseText)
2286
- return response;
2287
- const { redacted, found } = redactSecrets(responseText, config.piiTypes);
2288
- if (found.length === 0)
2289
- return response;
2290
- for (const v of found) {
2291
- config.onViolation?.(v);
2292
- }
2293
- return {
2294
- ...response,
2295
- message: {
2296
- ...response.message,
2297
- content: redacted
2956
+ function outputGuardrailMiddleware(config) {
2957
+ const mode = config.onViolation ?? "block";
2958
+ return async (ctx, next) => {
2959
+ const response = await next(ctx);
2960
+ const text = extractText(response.message.content);
2961
+ const violations = [];
2962
+ if (config.piiDetection) {
2963
+ violations.push(...detectPII(text));
2964
+ }
2965
+ if (config.contentPolicy) {
2966
+ violations.push(...checkContentPolicy(text, config.contentPolicy));
2967
+ }
2968
+ if (config.customRules?.length) {
2969
+ violations.push(...checkCustomRules(text, config.customRules));
2970
+ }
2971
+ if (violations.length === 0)
2972
+ return response;
2973
+ for (const v of violations) {
2974
+ config.onViolationCallback?.(v);
2975
+ }
2976
+ switch (mode) {
2977
+ case "block":
2978
+ throw ElsiumError.validation(`Output guardrail violation: ${violations.map((v) => v.detail).join("; ")}`, { violations });
2979
+ case "redact": {
2980
+ let redacted = text;
2981
+ if (config.piiDetection) {
2982
+ redacted = redactPII(redacted);
2983
+ }
2984
+ return {
2985
+ ...response,
2986
+ message: { ...response.message, content: redacted }
2987
+ };
2988
+ }
2989
+ case "warn":
2990
+ log5.warn("Output guardrail violations detected", { violations });
2991
+ return response;
2298
2992
  }
2299
2993
  };
2300
2994
  }
2301
- function securityMiddleware(config) {
2302
- return async (ctx, next) => {
2303
- for (const message of ctx.request.messages) {
2304
- const text = extractText(message.content);
2305
- if (!text)
2306
- continue;
2307
- const violations = scanMessageForViolations(text, config);
2308
- if (violations.length > 0) {
2309
- reportAndThrow(violations, config);
2995
+ // ../gateway/src/batch.ts
2996
+ var log6 = createLogger();
2997
+ function createBatch(gateway2, config) {
2998
+ const concurrency = config?.concurrency ?? 5;
2999
+ const retryPerItem = config?.retryPerItem ?? 0;
3000
+ return {
3001
+ async execute(requests) {
3002
+ const startTime = performance.now();
3003
+ const results = new Array(requests.length);
3004
+ let completed = 0;
3005
+ let totalSucceeded = 0;
3006
+ let totalFailed = 0;
3007
+ let running = 0;
3008
+ let nextIndex = 0;
3009
+ const signal = config?.signal;
3010
+ async function processItem(index) {
3011
+ if (signal?.aborted) {
3012
+ results[index] = {
3013
+ index,
3014
+ success: false,
3015
+ error: "Batch cancelled"
3016
+ };
3017
+ totalFailed++;
3018
+ return;
3019
+ }
3020
+ let lastError;
3021
+ for (let attempt = 0;attempt <= retryPerItem; attempt++) {
3022
+ try {
3023
+ const response = await gateway2.complete(requests[index]);
3024
+ results[index] = { index, success: true, response };
3025
+ totalSucceeded++;
3026
+ return;
3027
+ } catch (err2) {
3028
+ lastError = err2 instanceof Error ? err2.message : String(err2);
3029
+ if (attempt < retryPerItem && err2 instanceof ElsiumError && err2.retryable) {
3030
+ continue;
3031
+ }
3032
+ break;
3033
+ }
3034
+ }
3035
+ results[index] = { index, success: false, error: lastError };
3036
+ totalFailed++;
2310
3037
  }
3038
+ return new Promise((resolve) => {
3039
+ function scheduleNext() {
3040
+ while (running < concurrency && nextIndex < requests.length) {
3041
+ if (signal?.aborted) {
3042
+ for (let i = nextIndex;i < requests.length; i++) {
3043
+ results[i] = { index: i, success: false, error: "Batch cancelled" };
3044
+ totalFailed++;
3045
+ }
3046
+ nextIndex = requests.length;
3047
+ break;
3048
+ }
3049
+ const idx = nextIndex++;
3050
+ running++;
3051
+ processItem(idx).then(() => {
3052
+ running--;
3053
+ completed++;
3054
+ config?.onProgress?.(completed, requests.length);
3055
+ if (completed === requests.length) {
3056
+ resolve({
3057
+ results,
3058
+ totalSucceeded,
3059
+ totalFailed,
3060
+ totalDurationMs: Math.round(performance.now() - startTime)
3061
+ });
3062
+ } else {
3063
+ scheduleNext();
3064
+ }
3065
+ });
3066
+ }
3067
+ }
3068
+ if (requests.length === 0) {
3069
+ resolve({
3070
+ results: [],
3071
+ totalSucceeded: 0,
3072
+ totalFailed: 0,
3073
+ totalDurationMs: 0
3074
+ });
3075
+ return;
3076
+ }
3077
+ scheduleNext();
3078
+ });
2311
3079
  }
2312
- const response = await next(ctx);
2313
- if (config.secretRedaction !== false) {
2314
- return redactResponseSecrets(response, config);
2315
- }
2316
- return response;
2317
3080
  };
2318
3081
  }
2319
3082
  // ../gateway/src/router.ts
@@ -2544,12 +3307,26 @@ function createProviderMesh(config) {
2544
3307
  const available = sortedProviders.find((e) => isProviderAvailable(e.name));
2545
3308
  const entry = available ?? sortedProviders[0];
2546
3309
  const gw = getGateway(entry.name);
2547
- return gw.stream({ ...request, model: request.model ?? entry.model });
3310
+ let resolvedStream = null;
3311
+ callWithCircuitBreaker(entry.name, () => {
3312
+ resolvedStream = gw.stream({ ...request, model: request.model ?? entry.model });
3313
+ return Promise.resolve(resolvedStream);
3314
+ }).catch(() => {});
3315
+ if (resolvedStream === null) {
3316
+ const err2 = new ElsiumError({
3317
+ code: "PROVIDER_ERROR",
3318
+ message: "Circuit breaker is open",
3319
+ retryable: true
3320
+ });
3321
+ return new ElsiumStream(async function* () {
3322
+ yield { type: "error", error: err2 };
3323
+ }());
3324
+ }
3325
+ return resolvedStream;
2548
3326
  }
2549
3327
  };
2550
3328
  }
2551
3329
  // ../tools/src/define.ts
2552
- var log2 = createLogger();
2553
3330
  function defineTool(config) {
2554
3331
  const { name, description, input, output, handler, timeoutMs = 30000 } = config;
2555
3332
  return {
@@ -2624,91 +3401,20 @@ function defineTool(config) {
2624
3401
  }
2625
3402
  };
2626
3403
  }
2627
- function zodDefKind2(def) {
2628
- return typeof def.type === "string" ? def.type : def.typeName;
2629
- }
2630
- function zodObjectToJsonSchema(def) {
2631
- const shape = typeof def.shape === "function" ? def.shape() : def.shape;
2632
- const properties = {};
2633
- const required = [];
2634
- for (const [key, value] of Object.entries(shape)) {
2635
- const fieldSchema = value;
2636
- properties[key] = zodToJsonSchema(fieldSchema);
2637
- const fieldDef = fieldSchema._def;
2638
- const fieldKind = zodDefKind2(fieldDef);
2639
- if (fieldKind !== "optional" && fieldKind !== "ZodOptional" && fieldKind !== "default" && fieldKind !== "ZodDefault") {
2640
- required.push(key);
2641
- }
2642
- if (fieldDef.description) {
2643
- properties[key].description = fieldDef.description;
2644
- }
2645
- }
2646
- return { type: "object", properties, required };
2647
- }
2648
- function zodToJsonSchema(schema) {
2649
- if (!("_def" in schema))
2650
- return { type: "object" };
2651
- const def = schema._def;
2652
- const kind = zodDefKind2(def);
2653
- switch (kind) {
2654
- case "object":
2655
- case "ZodObject":
2656
- return zodObjectToJsonSchema(def);
2657
- case "string":
2658
- case "ZodString":
2659
- return { type: "string" };
2660
- case "number":
2661
- case "ZodNumber":
2662
- return { type: "number" };
2663
- case "boolean":
2664
- case "ZodBoolean":
2665
- return { type: "boolean" };
2666
- case "array":
2667
- case "ZodArray":
2668
- return {
2669
- type: "array",
2670
- items: zodToJsonSchema(def.element ?? def.type)
2671
- };
2672
- case "enum":
2673
- case "ZodEnum": {
2674
- const values = def.values ?? (def.entries ? Object.values(def.entries) : []);
2675
- return { type: "string", enum: values };
2676
- }
2677
- case "optional":
2678
- case "ZodOptional":
2679
- return zodToJsonSchema(def.innerType);
2680
- case "default":
2681
- case "ZodDefault":
2682
- return zodToJsonSchema(def.innerType);
2683
- case "nullable":
2684
- case "ZodNullable": {
2685
- const inner = zodToJsonSchema(def.innerType);
2686
- return { ...inner, nullable: true };
2687
- }
2688
- case "ZodLiteral":
2689
- return { type: typeof def.value, const: def.value };
2690
- case "ZodUnion": {
2691
- const options = def.options.map(zodToJsonSchema);
2692
- return { anyOf: options };
2693
- }
2694
- case "ZodRecord":
2695
- return {
2696
- type: "object",
2697
- additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : { type: "string" }
2698
- };
2699
- case "ZodTuple": {
2700
- const items = (def.items ?? []).map(zodToJsonSchema);
2701
- return { type: "array", prefixItems: items, minItems: items.length, maxItems: items.length };
2702
- }
2703
- case "ZodDate":
2704
- return { type: "string", format: "date-time" };
2705
- default:
2706
- log2.warn(`zodToJsonSchema: unsupported type ${kind}, defaulting to string`);
2707
- return { type: "string" };
2708
- }
2709
- }
2710
3404
  // ../tools/src/toolkit.ts
2711
3405
  function createToolkit(name, tools) {
3406
+ const seen = new Set;
3407
+ for (const tool of tools) {
3408
+ if (seen.has(tool.name)) {
3409
+ throw new ElsiumError({
3410
+ code: "CONFIG_ERROR",
3411
+ message: `Duplicate tool name "${tool.name}" in toolkit "${name}"`,
3412
+ retryable: false,
3413
+ metadata: { toolkit: name, tool: tool.name }
3414
+ });
3415
+ }
3416
+ seen.add(tool.name);
3417
+ }
2712
3418
  const toolMap = new Map(tools.map((t) => [t.name, t]));
2713
3419
  return {
2714
3420
  name,
@@ -6727,7 +7433,26 @@ var coerce = {
6727
7433
  };
6728
7434
  var NEVER = INVALID;
6729
7435
  // ../tools/src/builtin.ts
6730
- var BLOCKED_HOSTS = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0|0+\.0+\.0+\.0+|\[::1\]|\[::ffff:127\.\d+\.\d+\.\d+\]|::ffff:127\.\d+\.\d+\.\d+|0177\.\d+\.\d+\.\d+|2130706433|\[fd[0-9a-f]{2}:)/i;
7436
+ var BLOCKED_HOSTS = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0|0+\.0+\.0+\.0+|\[?::1\]?|\[?::ffff:127\.\d+\.\d+\.\d+\]?|\[?::ffff:10\.\d+\.\d+\.\d+\]?|\[?::ffff:192\.168\.\d+\.\d+\]?|\[?::ffff:172\.(1[6-9]|2\d|3[01])\.\d+\.\d+\]?|::ffff:127\.\d+\.\d+\.\d+|0177\.\d+\.\d+\.\d+|2130706433|\[?::\]?|\[?f[cd][0-9a-f]{2}:|\[?fe80:)/i;
7437
+ var BLOCKED_HEADER_NAMES = new Set([
7438
+ "cookie",
7439
+ "set-cookie",
7440
+ "authorization",
7441
+ "proxy-authorization",
7442
+ "host",
7443
+ "x-api-key",
7444
+ "x-forwarded-for",
7445
+ "x-real-ip"
7446
+ ]);
7447
+ function sanitizeHeaders(headers) {
7448
+ const safe = {};
7449
+ for (const [key, value] of Object.entries(headers)) {
7450
+ if (!BLOCKED_HEADER_NAMES.has(key.toLowerCase())) {
7451
+ safe[key] = value;
7452
+ }
7453
+ }
7454
+ return safe;
7455
+ }
6731
7456
  function validateUrl(urlString) {
6732
7457
  const parsed = new URL(urlString);
6733
7458
  if (!["http:", "https:"].includes(parsed.protocol)) {
@@ -6752,8 +7477,9 @@ var httpFetchTool = defineTool({
6752
7477
  timeoutMs: 15000,
6753
7478
  handler: async ({ url, headers }, context) => {
6754
7479
  validateUrl(url);
7480
+ const safeHeaders = headers ? sanitizeHeaders(headers) : undefined;
6755
7481
  const response = await fetch(url, {
6756
- headers,
7482
+ headers: safeHeaders,
6757
7483
  signal: context.signal,
6758
7484
  redirect: "manual"
6759
7485
  });
@@ -7228,15 +7954,33 @@ function createMemory(config) {
7228
7954
  case "unlimited":
7229
7955
  break;
7230
7956
  }
7957
+ if (config.store && config.agentId) {
7958
+ config.store.save(config.agentId, [...messages]).catch(() => {});
7959
+ }
7231
7960
  },
7232
7961
  getMessages() {
7233
7962
  return [...messages];
7234
7963
  },
7235
7964
  clear() {
7236
7965
  messages.length = 0;
7966
+ if (config.store && config.agentId) {
7967
+ config.store.clear(config.agentId).catch(() => {});
7968
+ }
7237
7969
  },
7238
7970
  getTokenEstimate() {
7239
7971
  return messages.reduce((sum, m) => sum + estimateTokens(m), 0);
7972
+ },
7973
+ async loadFromStore() {
7974
+ if (!config.store || !config.agentId)
7975
+ return;
7976
+ const stored = await config.store.load(config.agentId);
7977
+ messages.length = 0;
7978
+ messages.push(...stored);
7979
+ },
7980
+ async saveToStore() {
7981
+ if (!config.store || !config.agentId)
7982
+ return;
7983
+ await config.store.save(config.agentId, [...messages]);
7240
7984
  }
7241
7985
  };
7242
7986
  }
@@ -7282,7 +8026,7 @@ var JAILBREAK_PATTERNS2 = [
7282
8026
  detail: "Opposite mode jailbreak attempt"
7283
8027
  }
7284
8028
  ];
7285
- var SECRET_PATTERNS2 = [
8029
+ var SECRET_PATTERNS3 = [
7286
8030
  {
7287
8031
  pattern: /\bsk-[a-zA-Z0-9_-]{20,}\b/g,
7288
8032
  detail: "API secret key detected",
@@ -7360,7 +8104,7 @@ function createAgentSecurity(config) {
7360
8104
  const violations = [];
7361
8105
  let redactedOutput = output;
7362
8106
  if (config.redactSecrets !== false) {
7363
- for (const { pattern, detail, replacement } of SECRET_PATTERNS2) {
8107
+ for (const { pattern, detail, replacement } of SECRET_PATTERNS3) {
7364
8108
  const regex = new RegExp(pattern.source, pattern.flags);
7365
8109
  if (regex.test(redactedOutput)) {
7366
8110
  violations.push({ type: "secret_detected", detail, severity: "medium" });
@@ -7421,9 +8165,9 @@ Only respond with JSON, nothing else.`
7421
8165
  if (!parsed) {
7422
8166
  return null;
7423
8167
  }
7424
- const score = parsed.score ?? 0.5;
8168
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
7425
8169
  const threshold = config.hallucination?.threshold ?? 0.7;
7426
- const claims = parsed.hallucinated_claims ?? [];
8170
+ const claims = Array.isArray(parsed.hallucinated_claims) ? parsed.hallucinated_claims : [];
7427
8171
  return {
7428
8172
  passed: score >= threshold,
7429
8173
  score,
@@ -7486,12 +8230,12 @@ Only respond with JSON, nothing else.`
7486
8230
  if (!parsed) {
7487
8231
  return null;
7488
8232
  }
7489
- const score = parsed.score ?? 0.5;
8233
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
7490
8234
  const threshold = config.relevance?.threshold ?? 0.5;
7491
8235
  return {
7492
8236
  passed: score >= threshold,
7493
8237
  score,
7494
- reason: parsed.reason ?? (score >= threshold ? "Output is relevant" : "Output lacks relevance")
8238
+ reason: typeof parsed.reason === "string" ? parsed.reason : score >= threshold ? "Output is relevant" : "Output lacks relevance"
7495
8239
  };
7496
8240
  }
7497
8241
  function checkRelevanceHeuristic(input, output) {
@@ -7543,8 +8287,8 @@ Only respond with JSON, nothing else.`
7543
8287
  if (!parsed) {
7544
8288
  return null;
7545
8289
  }
7546
- const score = parsed.score ?? 0.5;
7547
- const claims = parsed.ungrounded_claims ?? [];
8290
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
8291
+ const claims = Array.isArray(parsed.ungrounded_claims) ? parsed.ungrounded_claims : [];
7548
8292
  return {
7549
8293
  passed: score >= 0.7,
7550
8294
  score,
@@ -7605,16 +8349,23 @@ Only respond with JSON, nothing else.`
7605
8349
  }
7606
8350
 
7607
8351
  // ../agents/src/state-machine.ts
8352
+ async function safeHook(fn) {
8353
+ if (!fn)
8354
+ return;
8355
+ try {
8356
+ await fn();
8357
+ } catch (_) {}
8358
+ }
7608
8359
  function executeStateMachine(baseConfig, stateConfig, deps, input, options) {
7609
8360
  return runStateMachine(baseConfig, stateConfig, deps, input, options ?? {});
7610
8361
  }
7611
- function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal) {
8362
+ function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal, hooks, approvalGate, approvalConfig) {
7612
8363
  const toolCalls = response.message.toolCalls;
7613
8364
  if (!toolCalls?.length || response.stopReason !== "tool_use") {
7614
8365
  return null;
7615
8366
  }
7616
8367
  return (async () => {
7617
- const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal);
8368
+ const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal, hooks, approvalGate, approvalConfig);
7618
8369
  const toolMessage = {
7619
8370
  role: "tool",
7620
8371
  content: "",
@@ -7718,6 +8469,7 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7718
8469
  });
7719
8470
  }
7720
8471
  const agentSecurity = baseConfig.guardrails?.security ? createAgentSecurity(baseConfig.guardrails.security) : null;
8472
+ const approvalGate = baseConfig.guardrails?.approval ? createApprovalGate(baseConfig.guardrails.approval) : null;
7721
8473
  const maxTokenBudget = guardrails?.maxTokenBudget ?? 500000;
7722
8474
  const outputValidator = guardrails?.outputValidator ?? (() => true);
7723
8475
  const conversationMessages = [{ role: "user", content: input }];
@@ -7735,7 +8487,8 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7735
8487
  checkTokenBudget(totalInputTokens + totalOutputTokens, maxTokenBudget);
7736
8488
  response = applyOutputGuardrails(response, outputValidator, agentSecurity);
7737
8489
  conversationMessages.push(response.message);
7738
- const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal);
8490
+ await safeHook(() => baseConfig.hooks?.onMessage?.(response.message));
8491
+ const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal, baseConfig.hooks, approvalGate, baseConfig.guardrails);
7739
8492
  if (toolCallAction) {
7740
8493
  await toolCallAction;
7741
8494
  continue;
@@ -7756,9 +8509,30 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7756
8509
  metadata: { iterations, maxIterations, lastState: currentStateName }
7757
8510
  });
7758
8511
  }
7759
- async function executeToolCalls(toolCalls, toolMap, history, signal) {
8512
+ async function executeToolCalls(toolCalls, toolMap, history, signal, hooks, approvalGate, approvalConfig) {
7760
8513
  const results = [];
7761
8514
  for (const tc of toolCalls) {
8515
+ await safeHook(() => hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
8516
+ if (approvalGate && shouldRequireApproval(approvalConfig?.approval?.requireApprovalFor, {
8517
+ toolName: tc.name
8518
+ })) {
8519
+ const decision = await approvalGate.requestApproval("tool_call", `Execute tool: ${tc.name}`, {
8520
+ toolName: tc.name,
8521
+ arguments: tc.arguments
8522
+ });
8523
+ if (!decision.approved) {
8524
+ const deniedResult = {
8525
+ success: false,
8526
+ error: `Tool call denied: ${decision.reason ?? "Approval denied"}`,
8527
+ toolCallId: tc.id,
8528
+ durationMs: 0
8529
+ };
8530
+ await safeHook(() => hooks?.onToolResult?.(deniedResult));
8531
+ history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
8532
+ results.push(formatToolResult(deniedResult));
8533
+ continue;
8534
+ }
8535
+ }
7762
8536
  const tool = toolMap.get(tc.name);
7763
8537
  if (!tool) {
7764
8538
  const errorResult = {
@@ -7767,11 +8541,13 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
7767
8541
  toolCallId: tc.id,
7768
8542
  durationMs: 0
7769
8543
  };
8544
+ await safeHook(() => hooks?.onToolResult?.(errorResult));
7770
8545
  history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
7771
8546
  results.push(formatToolResult(errorResult));
7772
8547
  continue;
7773
8548
  }
7774
8549
  const result = await tool.execute(tc.arguments, { toolCallId: tc.id, signal });
8550
+ await safeHook(() => hooks?.onToolResult?.(result));
7775
8551
  history.push({ name: tc.name, arguments: tc.arguments, result });
7776
8552
  results.push(formatToolResult(result));
7777
8553
  }
@@ -7779,7 +8555,7 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
7779
8555
  }
7780
8556
 
7781
8557
  // ../agents/src/agent.ts
7782
- async function safeHook(fn) {
8558
+ async function safeHook2(fn) {
7783
8559
  if (!fn)
7784
8560
  return;
7785
8561
  try {
@@ -7885,7 +8661,7 @@ function defineAgent(config, deps) {
7885
8661
  traceId,
7886
8662
  confidence
7887
8663
  };
7888
- await safeHook(() => config.hooks?.onComplete?.(agentResult));
8664
+ await safeHook2(() => config.hooks?.onComplete?.(agentResult));
7889
8665
  return { action: "return", result: agentResult };
7890
8666
  }
7891
8667
  function checkBudget(totalInputTokens, totalOutputTokens) {
@@ -7943,7 +8719,7 @@ function defineAgent(config, deps) {
7943
8719
  totalInputTokens += response.usage.inputTokens;
7944
8720
  totalOutputTokens += response.usage.outputTokens;
7945
8721
  totalCost += response.cost.totalCost;
7946
- await safeHook(() => config.hooks?.onMessage?.(response.message));
8722
+ await safeHook2(() => config.hooks?.onMessage?.(response.message));
7947
8723
  conversationMessages.push(response.message);
7948
8724
  if (!response.message.toolCalls?.length || response.stopReason !== "tool_use") {
7949
8725
  const result = await handleNonToolResponse(response, messages, iterations, totalInputTokens, totalOutputTokens, totalCost, toolCallHistory, traceId, conversationMessages);
@@ -7971,7 +8747,7 @@ function defineAgent(config, deps) {
7971
8747
  async function executeToolCalls2(toolCalls, history, options = {}) {
7972
8748
  const results = [];
7973
8749
  for (const tc of toolCalls) {
7974
- await safeHook(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
8750
+ await safeHook2(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
7975
8751
  if (approvalGate && shouldRequireApproval(config.guardrails?.approval?.requireApprovalFor, {
7976
8752
  toolName: tc.name
7977
8753
  })) {
@@ -7983,7 +8759,7 @@ function defineAgent(config, deps) {
7983
8759
  toolCallId: tc.id,
7984
8760
  durationMs: 0
7985
8761
  };
7986
- await safeHook(() => config.hooks?.onToolResult?.(deniedResult));
8762
+ await safeHook2(() => config.hooks?.onToolResult?.(deniedResult));
7987
8763
  history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
7988
8764
  results.push(formatToolResult(deniedResult));
7989
8765
  continue;
@@ -7997,7 +8773,7 @@ function defineAgent(config, deps) {
7997
8773
  toolCallId: tc.id,
7998
8774
  durationMs: 0
7999
8775
  };
8000
- await safeHook(() => config.hooks?.onToolResult?.(errorResult));
8776
+ await safeHook2(() => config.hooks?.onToolResult?.(errorResult));
8001
8777
  history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
8002
8778
  results.push(formatToolResult(errorResult));
8003
8779
  continue;
@@ -8006,7 +8782,7 @@ function defineAgent(config, deps) {
8006
8782
  toolCallId: tc.id,
8007
8783
  signal: options.signal
8008
8784
  });
8009
- await safeHook(() => config.hooks?.onToolResult?.(result));
8785
+ await safeHook2(() => config.hooks?.onToolResult?.(result));
8010
8786
  history.push({ name: tc.name, arguments: tc.arguments, result });
8011
8787
  results.push(formatToolResult(result));
8012
8788
  }
@@ -8029,15 +8805,113 @@ function defineAgent(config, deps) {
8029
8805
  continue;
8030
8806
  validateInputText(extractText(msg.content));
8031
8807
  }
8032
- if (config.states && config.initialState) {
8033
- const inputText = messages.filter((m) => m.role === "user").map((m) => extractText(m.content)).join(`
8034
- `);
8035
- return executeStateMachine(config, { states: config.states, initialState: config.initialState }, deps, inputText || "", options);
8808
+ if (config.states && config.initialState) {
8809
+ const inputText = messages.filter((m) => m.role === "user").map((m) => extractText(m.content)).join(`
8810
+ `);
8811
+ return executeStateMachine(config, { states: config.states, initialState: config.initialState }, deps, inputText || "", options);
8812
+ }
8813
+ return executeLoop(messages, options);
8814
+ },
8815
+ resetMemory() {
8816
+ memory.clear();
8817
+ }
8818
+ };
8819
+ }
8820
+ // ../agents/src/stores/memory-store.ts
8821
+ function createInMemoryMemoryStore() {
8822
+ const store = new Map;
8823
+ return {
8824
+ async load(agentId) {
8825
+ return [...store.get(agentId) ?? []];
8826
+ },
8827
+ async save(agentId, messages) {
8828
+ store.set(agentId, [...messages]);
8829
+ },
8830
+ async clear(agentId) {
8831
+ store.delete(agentId);
8832
+ }
8833
+ };
8834
+ }
8835
+ // ../agents/src/stores/sqlite-store.ts
8836
+ import { createRequire } from "node:module";
8837
+ var require2 = createRequire(import.meta.url);
8838
+ var log7 = createLogger();
8839
+ var BLOCKED_KEYS2 = new Set(["__proto__", "constructor", "prototype"]);
8840
+ var TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8841
+ function createSqliteMemoryStore(config) {
8842
+ const { path, tableName = "agent_memory" } = config;
8843
+ if (BLOCKED_KEYS2.has(tableName)) {
8844
+ throw new Error(`Invalid table name: ${tableName}`);
8845
+ }
8846
+ if (!TABLE_NAME_PATTERN.test(tableName)) {
8847
+ throw new Error(`Invalid table name format: ${tableName}`);
8848
+ }
8849
+ let db = null;
8850
+ let initPromise = null;
8851
+ async function getDb() {
8852
+ if (db)
8853
+ return db;
8854
+ if (initPromise)
8855
+ return initPromise;
8856
+ initPromise = (async () => {
8857
+ try {
8858
+ const Database = require2("better-sqlite3");
8859
+ db = new Database(path);
8860
+ db.exec(`
8861
+ CREATE TABLE IF NOT EXISTS ${tableName} (
8862
+ agent_id TEXT NOT NULL,
8863
+ idx INTEGER NOT NULL,
8864
+ role TEXT NOT NULL,
8865
+ content TEXT NOT NULL,
8866
+ metadata TEXT,
8867
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
8868
+ PRIMARY KEY (agent_id, idx)
8869
+ )
8870
+ `);
8871
+ return db;
8872
+ } catch (err2) {
8873
+ initPromise = null;
8874
+ log7.error("Failed to initialize SQLite memory store", {
8875
+ error: err2 instanceof Error ? err2.message : String(err2)
8876
+ });
8877
+ throw new Error("better-sqlite3 is required for SQLite memory store. Install it as a dependency.");
8878
+ }
8879
+ })();
8880
+ return initPromise;
8881
+ }
8882
+ return {
8883
+ async load(agentId) {
8884
+ if (BLOCKED_KEYS2.has(agentId))
8885
+ return [];
8886
+ const database = await getDb();
8887
+ const rows = database.prepare(`SELECT role, content, metadata FROM ${tableName} WHERE agent_id = ? ORDER BY idx`).all(agentId);
8888
+ return rows.map((row) => {
8889
+ const msg = {
8890
+ role: row.role,
8891
+ content: JSON.parse(row.content)
8892
+ };
8893
+ if (row.metadata) {
8894
+ msg.metadata = JSON.parse(row.metadata);
8895
+ }
8896
+ return msg;
8897
+ });
8898
+ },
8899
+ async save(agentId, messages) {
8900
+ if (BLOCKED_KEYS2.has(agentId))
8901
+ return;
8902
+ const database = await getDb();
8903
+ database.prepare(`DELETE FROM ${tableName} WHERE agent_id = ?`).run(agentId);
8904
+ const insert = database.prepare(`INSERT INTO ${tableName} (agent_id, idx, role, content, metadata) VALUES (?, ?, ?, ?, ?)`);
8905
+ for (let i = 0;i < messages.length; i++) {
8906
+ const msg = messages[i];
8907
+ insert.run(agentId, i, msg.role, JSON.stringify(msg.content), msg.metadata ? JSON.stringify(msg.metadata) : null);
8036
8908
  }
8037
- return executeLoop(messages, options);
8038
8909
  },
8039
- resetMemory() {
8040
- memory.clear();
8910
+ async clear(agentId) {
8911
+ if (BLOCKED_KEYS2.has(agentId))
8912
+ return;
8913
+ const database = await getDb();
8914
+ database.prepare(`DELETE FROM ${tableName} WHERE agent_id = ?`).run(agentId);
8041
8915
  }
8042
8916
  };
8043
8917
  }
@@ -8538,7 +9412,11 @@ function createMockEmbeddings(dims = 128) {
8538
9412
  }
8539
9413
  };
8540
9414
  }
9415
+ var embeddingProviderRegistry = createRegistry("embeddingProvider");
8541
9416
  function getEmbeddingProvider(config) {
9417
+ const registered = embeddingProviderRegistry.get(config.provider);
9418
+ if (registered)
9419
+ return registered(config);
8542
9420
  switch (config.provider) {
8543
9421
  case "openai":
8544
9422
  return createOpenAIEmbeddings(config);
@@ -8547,12 +9425,13 @@ function getEmbeddingProvider(config) {
8547
9425
  default:
8548
9426
  throw new ElsiumError({
8549
9427
  code: "CONFIG_ERROR",
8550
- message: `Unknown embedding provider: ${config.provider}`,
9428
+ message: `Unknown embedding provider: ${config.provider}. Available: openai, mock${embeddingProviderRegistry.list().length ? `, ${embeddingProviderRegistry.list().join(", ")}` : ""}`,
8551
9429
  retryable: false
8552
9430
  });
8553
9431
  }
8554
9432
  }
8555
9433
  // ../rag/src/vectorstore.ts
9434
+ var vectorStoreRegistry = createRegistry("vectorStore");
8556
9435
  function cosineSimilarity(a, b) {
8557
9436
  if (a.length !== b.length)
8558
9437
  return 0;
@@ -8686,6 +9565,123 @@ function rag(config) {
8686
9565
  }
8687
9566
  };
8688
9567
  }
9568
+ // ../rag/src/stores/pgvector.ts
9569
+ import { createRequire as createRequire2 } from "node:module";
9570
+ var require3 = createRequire2(import.meta.url);
9571
+ var log8 = createLogger();
9572
+ var BLOCKED_KEYS3 = new Set(["__proto__", "constructor", "prototype"]);
9573
+ var TABLE_NAME_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
9574
+ function createPgVectorStore(config) {
9575
+ const { connectionString, tableName = "vector_chunks", dimensions = 1536 } = config;
9576
+ if (BLOCKED_KEYS3.has(tableName)) {
9577
+ throw new Error(`Invalid table name: ${tableName}`);
9578
+ }
9579
+ if (!TABLE_NAME_PATTERN2.test(tableName)) {
9580
+ throw new Error(`Invalid table name format: ${tableName}`);
9581
+ }
9582
+ let client = null;
9583
+ let initialized = false;
9584
+ async function getClient() {
9585
+ if (client)
9586
+ return client;
9587
+ try {
9588
+ const pg = require3("pg");
9589
+ client = new pg.Client({ connectionString });
9590
+ await client.connect();
9591
+ if (!initialized) {
9592
+ await client.query("CREATE EXTENSION IF NOT EXISTS vector");
9593
+ await client.query(`
9594
+ CREATE TABLE IF NOT EXISTS ${tableName} (
9595
+ id TEXT PRIMARY KEY,
9596
+ content TEXT NOT NULL,
9597
+ document_id TEXT NOT NULL,
9598
+ chunk_index INTEGER NOT NULL,
9599
+ metadata JSONB DEFAULT '{}',
9600
+ embedding vector(${dimensions})
9601
+ )
9602
+ `);
9603
+ initialized = true;
9604
+ }
9605
+ return client;
9606
+ } catch (err2) {
9607
+ log8.error("Failed to initialize PgVector store", {
9608
+ error: err2 instanceof Error ? err2.message : String(err2)
9609
+ });
9610
+ throw new Error("pg is required for PgVector store. Install it as a dependency.");
9611
+ }
9612
+ }
9613
+ return {
9614
+ name: "pgvector",
9615
+ async upsert(chunks) {
9616
+ const pg = await getClient();
9617
+ for (const chunk of chunks) {
9618
+ if (BLOCKED_KEYS3.has(chunk.id))
9619
+ continue;
9620
+ const embedding = `[${chunk.embedding.values.join(",")}]`;
9621
+ await pg.query(`INSERT INTO ${tableName} (id, content, document_id, chunk_index, metadata, embedding)
9622
+ VALUES ($1, $2, $3, $4, $5, $6)
9623
+ ON CONFLICT (id) DO UPDATE SET
9624
+ content = EXCLUDED.content,
9625
+ document_id = EXCLUDED.document_id,
9626
+ chunk_index = EXCLUDED.chunk_index,
9627
+ metadata = EXCLUDED.metadata,
9628
+ embedding = EXCLUDED.embedding`, [
9629
+ chunk.id,
9630
+ chunk.content,
9631
+ chunk.documentId,
9632
+ chunk.index,
9633
+ JSON.stringify(chunk.metadata),
9634
+ embedding
9635
+ ]);
9636
+ }
9637
+ },
9638
+ async query(embedding, options) {
9639
+ const pg = await getClient();
9640
+ const topK = options?.topK ?? 5;
9641
+ const minScore = options?.minScore ?? 0;
9642
+ const embeddingStr = `[${embedding.values.join(",")}]`;
9643
+ const result = await pg.query(`SELECT id, content, document_id, chunk_index, metadata,
9644
+ 1 - (embedding <=> $1::vector) as score
9645
+ FROM ${tableName}
9646
+ WHERE 1 - (embedding <=> $1::vector) >= $2
9647
+ ORDER BY embedding <=> $1::vector
9648
+ LIMIT $3`, [embeddingStr, minScore, topK]);
9649
+ return result.rows.map((row) => ({
9650
+ chunk: {
9651
+ id: row.id,
9652
+ content: row.content,
9653
+ documentId: row.document_id,
9654
+ index: row.chunk_index,
9655
+ metadata: {
9656
+ startChar: 0,
9657
+ endChar: 0,
9658
+ tokenEstimate: 0,
9659
+ ...row.metadata ?? {}
9660
+ }
9661
+ },
9662
+ score: row.score,
9663
+ distance: 1 - row.score
9664
+ }));
9665
+ },
9666
+ async delete(ids) {
9667
+ const pg = await getClient();
9668
+ const filtered = ids.filter((id) => !BLOCKED_KEYS3.has(id));
9669
+ if (filtered.length === 0)
9670
+ return;
9671
+ const placeholders = filtered.map((_, i) => `$${i + 1}`).join(", ");
9672
+ await pg.query(`DELETE FROM ${tableName} WHERE id IN (${placeholders})`, filtered);
9673
+ },
9674
+ async clear() {
9675
+ const pg = await getClient();
9676
+ await pg.query(`DELETE FROM ${tableName}`);
9677
+ },
9678
+ async count() {
9679
+ const pg = await getClient();
9680
+ const result = await pg.query(`SELECT COUNT(*)::int as count FROM ${tableName}`);
9681
+ return result.rows[0]?.count ?? 0;
9682
+ }
9683
+ };
9684
+ }
8689
9685
  // ../workflows/src/step.ts
8690
9686
  function step(name, config) {
8691
9687
  return { name, ...config };
@@ -9267,7 +10263,8 @@ function createCostEngine(config = {}) {
9267
10263
  };
9268
10264
  }
9269
10265
  // ../observe/src/tracer.ts
9270
- var log3 = createLogger();
10266
+ import { writeFileSync } from "node:fs";
10267
+ var log9 = createLogger();
9271
10268
  function observe(config = {}) {
9272
10269
  const {
9273
10270
  output = ["console"],
@@ -9282,7 +10279,21 @@ function observe(config = {}) {
9282
10279
  for (const out of output) {
9283
10280
  if (out === "console") {
9284
10281
  handlers.push(consoleHandler);
9285
- } else if (out === "json-file") {} else {
10282
+ } else if (out === "json-file") {
10283
+ exporters.push({
10284
+ name: "json-file",
10285
+ export(spansToExport) {
10286
+ const filename = `.elsium/traces-${Date.now()}.json`;
10287
+ try {
10288
+ writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
10289
+ } catch (err2) {
10290
+ log9.error("Failed to write trace file", {
10291
+ error: err2 instanceof Error ? err2.message : String(err2)
10292
+ });
10293
+ }
10294
+ }
10295
+ });
10296
+ } else {
9286
10297
  exporters.push(out);
9287
10298
  }
9288
10299
  }
@@ -9351,7 +10362,7 @@ function observe(config = {}) {
9351
10362
  function consoleHandler(span) {
9352
10363
  const duration = span.durationMs !== undefined ? `${span.durationMs}ms` : "running";
9353
10364
  const status = span.status === "error" ? "[ERROR]" : span.status === "ok" ? "[OK]" : "[...]";
9354
- log3.info("span", {
10365
+ log9.info("span", {
9355
10366
  trace: span.traceId,
9356
10367
  span: span.name,
9357
10368
  kind: span.kind,
@@ -9449,8 +10460,86 @@ function createMetrics(options) {
9449
10460
  }
9450
10461
  };
9451
10462
  }
10463
+ // ../observe/src/experiment.ts
10464
+ import { createHash as createHash2 } from "node:crypto";
10465
+ var log10 = createLogger();
10466
+ function createExperiment(config) {
10467
+ const { name, variants } = config;
10468
+ if (variants.length === 0) {
10469
+ throw new Error("Experiment must have at least one variant");
10470
+ }
10471
+ const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
10472
+ const stats = {};
10473
+ for (const v of variants) {
10474
+ stats[v.name] = { assignments: 0, metrics: {} };
10475
+ }
10476
+ function hashAssign(userId) {
10477
+ const hash = createHash2("sha256").update(`${name}:${userId}`).digest();
10478
+ const value = hash.readUInt32BE(0) % 1e4 / 1e4;
10479
+ return pickVariant(value);
10480
+ }
10481
+ function randomAssign() {
10482
+ const value = Math.random();
10483
+ return pickVariant(value);
10484
+ }
10485
+ function pickVariant(value) {
10486
+ let cumulative = 0;
10487
+ for (const v of variants) {
10488
+ cumulative += v.weight / totalWeight;
10489
+ if (value < cumulative)
10490
+ return v;
10491
+ }
10492
+ return variants[variants.length - 1];
10493
+ }
10494
+ return {
10495
+ assign(userId) {
10496
+ const variant = userId ? hashAssign(userId) : randomAssign();
10497
+ const s = stats[variant.name];
10498
+ if (s)
10499
+ s.assignments++;
10500
+ log10.debug("Experiment assignment", {
10501
+ experiment: name,
10502
+ variant: variant.name,
10503
+ userId
10504
+ });
10505
+ return variant;
10506
+ },
10507
+ record(variant, metrics) {
10508
+ const s = stats[variant];
10509
+ if (!s)
10510
+ return;
10511
+ for (const [key, value] of Object.entries(metrics)) {
10512
+ if (!s.metrics[key]) {
10513
+ s.metrics[key] = { sum: 0, count: 0 };
10514
+ }
10515
+ s.metrics[key].sum += value;
10516
+ s.metrics[key].count++;
10517
+ }
10518
+ },
10519
+ results() {
10520
+ let totalAssignments = 0;
10521
+ const variantResults = {};
10522
+ for (const [vName, s] of Object.entries(stats)) {
10523
+ totalAssignments += s.assignments;
10524
+ const metricsResult = {};
10525
+ for (const [key, m] of Object.entries(s.metrics)) {
10526
+ metricsResult[key] = {
10527
+ sum: m.sum,
10528
+ count: m.count,
10529
+ avg: m.count > 0 ? m.sum / m.count : 0
10530
+ };
10531
+ }
10532
+ variantResults[vName] = {
10533
+ assignments: s.assignments,
10534
+ metrics: metricsResult
10535
+ };
10536
+ }
10537
+ return { name, totalAssignments, variants: variantResults };
10538
+ }
10539
+ };
10540
+ }
9452
10541
  // ../observe/src/otel.ts
9453
- var log4 = createLogger();
10542
+ var log11 = createLogger();
9454
10543
  var SPAN_KIND_MAP = {
9455
10544
  llm: 3,
9456
10545
  tool: 1,
@@ -9591,10 +10680,10 @@ function createOTLPExporter(config) {
9591
10680
  body: JSON.stringify(payload)
9592
10681
  });
9593
10682
  if (!response.ok) {
9594
- log4.error(`OTLP export failed: ${response.status} ${response.statusText}`);
10683
+ log11.error(`OTLP export failed: ${response.status} ${response.statusText}`);
9595
10684
  }
9596
10685
  } catch (err2) {
9597
- log4.error("OTLP export error", { error: err2 instanceof Error ? err2.message : String(err2) });
10686
+ log11.error("OTLP export error", { error: err2 instanceof Error ? err2.message : String(err2) });
9598
10687
  }
9599
10688
  }
9600
10689
  function startAutoFlush() {
@@ -9617,6 +10706,16 @@ function createOTLPExporter(config) {
9617
10706
  } else {
9618
10707
  startAutoFlush();
9619
10708
  }
10709
+ },
10710
+ async shutdown() {
10711
+ if (flushTimer) {
10712
+ clearInterval(flushTimer);
10713
+ flushTimer = null;
10714
+ }
10715
+ if (buffer.length > 0) {
10716
+ const batch = buffer.splice(0, buffer.length);
10717
+ await sendBatch(batch);
10718
+ }
9620
10719
  }
9621
10720
  };
9622
10721
  }
@@ -11692,7 +12791,7 @@ var Hono2 = class extends Hono {
11692
12791
  // ../app/src/middleware.ts
11693
12792
  import { timingSafeEqual } from "node:crypto";
11694
12793
  function corsMiddleware(config = true) {
11695
- const opts = typeof config === "boolean" ? { origin: [], methods: ["GET", "POST", "OPTIONS"] } : config;
12794
+ const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
11696
12795
  return async (c, next) => {
11697
12796
  const requestOrigin = c.req.header("Origin") ?? "";
11698
12797
  let allowedOrigin;
@@ -11769,8 +12868,203 @@ function rateLimitMiddleware(config) {
11769
12868
  await next();
11770
12869
  };
11771
12870
  }
12871
+ function requestIdMiddleware() {
12872
+ return async (c, next) => {
12873
+ const raw2 = c.req.header("X-Request-ID");
12874
+ const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
12875
+ c.set("requestId", id);
12876
+ await next();
12877
+ c.res.headers.set("X-Request-ID", id);
12878
+ };
12879
+ }
12880
+ function requestLoggerMiddleware(logger) {
12881
+ const log12 = logger ?? createLogger();
12882
+ return async (c, next) => {
12883
+ const start = Date.now();
12884
+ await next();
12885
+ const duration = Date.now() - start;
12886
+ log12.info(`${c.req.method} ${c.req.path}`, {
12887
+ method: c.req.method,
12888
+ path: c.req.path,
12889
+ status: c.res.status,
12890
+ durationMs: duration,
12891
+ requestId: c.get("requestId")
12892
+ });
12893
+ };
12894
+ }
12895
+
12896
+ // ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/utils/stream.js
12897
+ var StreamingApi = class {
12898
+ writer;
12899
+ encoder;
12900
+ writable;
12901
+ abortSubscribers = [];
12902
+ responseReadable;
12903
+ aborted = false;
12904
+ closed = false;
12905
+ constructor(writable, _readable) {
12906
+ this.writable = writable;
12907
+ this.writer = writable.getWriter();
12908
+ this.encoder = new TextEncoder;
12909
+ const reader = _readable.getReader();
12910
+ this.abortSubscribers.push(async () => {
12911
+ await reader.cancel();
12912
+ });
12913
+ this.responseReadable = new ReadableStream({
12914
+ async pull(controller) {
12915
+ const { done, value } = await reader.read();
12916
+ done ? controller.close() : controller.enqueue(value);
12917
+ },
12918
+ cancel: () => {
12919
+ this.abort();
12920
+ }
12921
+ });
12922
+ }
12923
+ async write(input) {
12924
+ try {
12925
+ if (typeof input === "string") {
12926
+ input = this.encoder.encode(input);
12927
+ }
12928
+ await this.writer.write(input);
12929
+ } catch {}
12930
+ return this;
12931
+ }
12932
+ async writeln(input) {
12933
+ await this.write(input + `
12934
+ `);
12935
+ return this;
12936
+ }
12937
+ sleep(ms) {
12938
+ return new Promise((res) => setTimeout(res, ms));
12939
+ }
12940
+ async close() {
12941
+ try {
12942
+ await this.writer.close();
12943
+ } catch {}
12944
+ this.closed = true;
12945
+ }
12946
+ async pipe(body) {
12947
+ this.writer.releaseLock();
12948
+ await body.pipeTo(this.writable, { preventClose: true });
12949
+ this.writer = this.writable.getWriter();
12950
+ }
12951
+ onAbort(listener) {
12952
+ this.abortSubscribers.push(listener);
12953
+ }
12954
+ abort() {
12955
+ if (!this.aborted) {
12956
+ this.aborted = true;
12957
+ this.abortSubscribers.forEach((subscriber) => subscriber());
12958
+ }
12959
+ }
12960
+ };
12961
+
12962
+ // ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/utils.js
12963
+ var isOldBunVersion = () => {
12964
+ const version = typeof Bun !== "undefined" ? Bun.version : undefined;
12965
+ if (version === undefined) {
12966
+ return false;
12967
+ }
12968
+ const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
12969
+ isOldBunVersion = () => result;
12970
+ return result;
12971
+ };
12972
+
12973
+ // ../../node_modules/.bun/hono@4.12.3/node_modules/hono/dist/helper/streaming/stream.js
12974
+ var contextStash = /* @__PURE__ */ new WeakMap;
12975
+ var stream = (c, cb, onError) => {
12976
+ const { readable, writable } = new TransformStream;
12977
+ const stream2 = new StreamingApi(writable, readable);
12978
+ if (isOldBunVersion()) {
12979
+ c.req.raw.signal.addEventListener("abort", () => {
12980
+ if (!stream2.closed) {
12981
+ stream2.abort();
12982
+ }
12983
+ });
12984
+ }
12985
+ contextStash.set(stream2.responseReadable, c);
12986
+ (async () => {
12987
+ try {
12988
+ await cb(stream2);
12989
+ } catch (e) {
12990
+ if (e === undefined) {} else if (e instanceof Error && onError) {
12991
+ await onError(e, stream2);
12992
+ } else {
12993
+ console.error(e);
12994
+ }
12995
+ } finally {
12996
+ stream2.close();
12997
+ }
12998
+ })();
12999
+ return c.newResponse(stream2.responseReadable);
13000
+ };
13001
+
13002
+ // ../app/src/sse.ts
13003
+ function sseHeaders() {
13004
+ return {
13005
+ "Content-Type": "text/event-stream",
13006
+ "Cache-Control": "no-cache",
13007
+ Connection: "keep-alive",
13008
+ "X-Accel-Buffering": "no"
13009
+ };
13010
+ }
13011
+ function formatSSE(event, data) {
13012
+ const json = JSON.stringify(data);
13013
+ if (event === "message") {
13014
+ return `data: ${json}
13015
+
13016
+ `;
13017
+ }
13018
+ return `event: ${event}
13019
+ data: ${json}
13020
+
13021
+ `;
13022
+ }
13023
+ function streamResponse(c, source) {
13024
+ const headers = sseHeaders();
13025
+ for (const [key, value] of Object.entries(headers)) {
13026
+ c.header(key, value);
13027
+ }
13028
+ return stream(c, async (s) => {
13029
+ try {
13030
+ for await (const event of source) {
13031
+ const sseData = formatSSE("message", event);
13032
+ await s.write(sseData);
13033
+ }
13034
+ } catch (err2) {
13035
+ const errorEvent = {
13036
+ type: "error",
13037
+ error: err2 instanceof Error ? err2 : new Error(String(err2))
13038
+ };
13039
+ const sseData = formatSSE("error", {
13040
+ type: "error",
13041
+ message: errorEvent.error.message
13042
+ });
13043
+ await s.write(sseData);
13044
+ }
13045
+ });
13046
+ }
11772
13047
 
11773
13048
  // ../app/src/routes.ts
13049
+ function parseJsonBody(raw2) {
13050
+ try {
13051
+ return { ok: true, data: JSON.parse(raw2) };
13052
+ } catch {
13053
+ return { ok: false };
13054
+ }
13055
+ }
13056
+ function elsiumErrorResponse(c, err2, fallbackMessage) {
13057
+ if (err2 instanceof ElsiumError) {
13058
+ return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
13059
+ }
13060
+ return c.json({ error: fallbackMessage }, 500);
13061
+ }
13062
+ function resolveAgent(name, agents, defaultAgent) {
13063
+ const agent = name ? agents.get(name) : defaultAgent;
13064
+ if (agent)
13065
+ return { agent };
13066
+ return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
13067
+ }
11774
13068
  function createRoutes(deps) {
11775
13069
  const app = new Hono2;
11776
13070
  let totalRequests = 0;
@@ -11811,17 +13105,32 @@ function createRoutes(deps) {
11811
13105
  if (rawText.length > MAX_BODY_SIZE) {
11812
13106
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
11813
13107
  }
11814
- const body = JSON.parse(rawText);
13108
+ const parsed = parseJsonBody(rawText);
13109
+ if (!parsed.ok) {
13110
+ return c.json({ error: "Invalid JSON in request body" }, 400);
13111
+ }
13112
+ const body = parsed.data;
11815
13113
  if (!body.message) {
11816
13114
  return c.json({ error: "message is required" }, 400);
11817
13115
  }
11818
- const agent = body.agent ? deps.agents.get(body.agent) : deps.defaultAgent;
11819
- if (!agent) {
11820
- return c.json({
11821
- error: body.agent ? `Agent "${body.agent}" not found` : "No default agent configured"
11822
- }, 404);
13116
+ const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
13117
+ if ("error" in resolved) {
13118
+ return c.json({ error: resolved.error }, 404);
13119
+ }
13120
+ if (body.stream) {
13121
+ const stream2 = deps.gateway.stream({
13122
+ messages: [{ role: "user", content: body.message }],
13123
+ system: resolved.agent.config.system,
13124
+ model: resolved.agent.config.model
13125
+ });
13126
+ return streamResponse(c, stream2);
13127
+ }
13128
+ let result;
13129
+ try {
13130
+ result = await resolved.agent.run(body.message);
13131
+ } catch (err2) {
13132
+ return elsiumErrorResponse(c, err2, "Agent execution failed");
11823
13133
  }
11824
- const result = await agent.run(body.message);
11825
13134
  deps.tracer?.trackLLMCall({
11826
13135
  model: "unknown",
11827
13136
  inputTokens: result.usage.totalInputTokens,
@@ -11838,7 +13147,7 @@ function createRoutes(deps) {
11838
13147
  totalTokens: result.usage.totalTokens,
11839
13148
  cost: result.usage.totalCost
11840
13149
  },
11841
- model: agent.config.model ?? "default",
13150
+ model: resolved.agent.config.model ?? "default",
11842
13151
  traceId: result.traceId
11843
13152
  };
11844
13153
  return c.json(response);
@@ -11850,7 +13159,11 @@ function createRoutes(deps) {
11850
13159
  if (rawText.length > MAX_BODY_SIZE) {
11851
13160
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
11852
13161
  }
11853
- const body = JSON.parse(rawText);
13162
+ const parsed = parseJsonBody(rawText);
13163
+ if (!parsed.ok) {
13164
+ return c.json({ error: "Invalid JSON in request body" }, 400);
13165
+ }
13166
+ const body = parsed.data;
11854
13167
  if (!body.messages?.length) {
11855
13168
  return c.json({ error: "messages array is required" }, 400);
11856
13169
  }
@@ -11858,13 +13171,28 @@ function createRoutes(deps) {
11858
13171
  role: m.role,
11859
13172
  content: m.content
11860
13173
  }));
11861
- const response = await deps.gateway.complete({
11862
- messages,
11863
- model: body.model,
11864
- system: body.system,
11865
- maxTokens: body.maxTokens,
11866
- temperature: body.temperature
11867
- });
13174
+ if (body.stream) {
13175
+ const stream2 = deps.gateway.stream({
13176
+ messages,
13177
+ model: body.model,
13178
+ system: body.system,
13179
+ maxTokens: body.maxTokens,
13180
+ temperature: body.temperature
13181
+ });
13182
+ return streamResponse(c, stream2);
13183
+ }
13184
+ let response;
13185
+ try {
13186
+ response = await deps.gateway.complete({
13187
+ messages,
13188
+ model: body.model,
13189
+ system: body.system,
13190
+ maxTokens: body.maxTokens,
13191
+ temperature: body.temperature
13192
+ });
13193
+ } catch (err2) {
13194
+ return elsiumErrorResponse(c, err2, "Completion failed");
13195
+ }
11868
13196
  deps.tracer?.trackLLMCall({
11869
13197
  model: response.model,
11870
13198
  inputTokens: response.usage.inputTokens,
@@ -11893,9 +13221,18 @@ function createRoutes(deps) {
11893
13221
  }
11894
13222
 
11895
13223
  // ../app/src/app.ts
11896
- var log5 = createLogger();
13224
+ var log12 = createLogger();
11897
13225
  function createApp(config) {
11898
13226
  const app = new Hono2;
13227
+ app.onError((err2, c) => {
13228
+ const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
13229
+ const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
13230
+ log12.error("Unhandled error", { error: err2.message, code, path: c.req.path });
13231
+ return c.json({ error: err2.message, code }, statusCode);
13232
+ });
13233
+ app.notFound((c) => {
13234
+ return c.json({ error: "Not found" }, 404);
13235
+ });
11899
13236
  const providerNames = Object.keys(config.gateway.providers);
11900
13237
  const primaryProvider = providerNames[0];
11901
13238
  const primaryConfig = config.gateway.providers[primaryProvider];
@@ -11910,6 +13247,8 @@ function createApp(config) {
11910
13247
  costTracking: config.observe?.costTracking ?? true
11911
13248
  });
11912
13249
  const serverConfig = config.server ?? {};
13250
+ app.use("*", requestIdMiddleware());
13251
+ app.use("*", requestLoggerMiddleware(log12));
11913
13252
  if (serverConfig.cors) {
11914
13253
  app.use("*", corsMiddleware(serverConfig.cors));
11915
13254
  }
@@ -11932,7 +13271,7 @@ function createApp(config) {
11932
13271
  defaultAgent,
11933
13272
  tracer,
11934
13273
  startTime: Date.now(),
11935
- version: "0.1.0",
13274
+ version: config.version ?? "0.2.2",
11936
13275
  providers: providerNames
11937
13276
  });
11938
13277
  app.route("/", routes);
@@ -11948,13 +13287,25 @@ function createApp(config) {
11948
13287
  port: listenPort,
11949
13288
  hostname
11950
13289
  });
11951
- log5.info("ElsiumAI server started", {
13290
+ let shutdownManager;
13291
+ if (serverConfig.gracefulShutdown) {
13292
+ const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
13293
+ shutdownManager = createShutdownManager({
13294
+ drainTimeoutMs,
13295
+ onDrainStart: () => log12.info("Draining connections..."),
13296
+ onDrainComplete: () => log12.info("Drain complete")
13297
+ });
13298
+ }
13299
+ log12.info("ElsiumAI server started", {
11952
13300
  url: `http://${hostname}:${listenPort}`,
11953
13301
  routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
11954
13302
  });
11955
13303
  return {
11956
13304
  port: listenPort,
11957
- stop: () => {
13305
+ stop: async () => {
13306
+ if (shutdownManager) {
13307
+ await shutdownManager.shutdown();
13308
+ }
11958
13309
  server.close();
11959
13310
  }
11960
13311
  };
@@ -11962,7 +13313,54 @@ function createApp(config) {
11962
13313
  };
11963
13314
  }
11964
13315
  // ../app/src/rbac.ts
11965
- var log6 = createLogger();
13316
+ var log13 = createLogger();
13317
+ // ../app/src/tenant.ts
13318
+ var log14 = createLogger();
13319
+ function tenantMiddleware(config) {
13320
+ const { extractTenant, onUnknownTenant = "reject", defaultTenant } = config;
13321
+ return async (c, next) => {
13322
+ const tenant = extractTenant(c);
13323
+ if (!tenant) {
13324
+ if (onUnknownTenant === "default" && defaultTenant) {
13325
+ c.set("tenant", defaultTenant);
13326
+ log14.debug("Using default tenant", { tenantId: defaultTenant.tenantId });
13327
+ } else {
13328
+ return c.json({ error: "Tenant identification required" }, 401);
13329
+ }
13330
+ } else {
13331
+ c.set("tenant", tenant);
13332
+ log14.debug("Tenant identified", { tenantId: tenant.tenantId });
13333
+ }
13334
+ await next();
13335
+ };
13336
+ }
13337
+ function tenantRateLimitMiddleware() {
13338
+ const windows = new Map;
13339
+ return async (c, next) => {
13340
+ const tenant = c.get("tenant");
13341
+ if (!tenant?.limits?.maxRequestsPerMinute) {
13342
+ await next();
13343
+ return;
13344
+ }
13345
+ const limit = tenant.limits.maxRequestsPerMinute;
13346
+ const now = Date.now();
13347
+ const windowMs = 60000;
13348
+ const key = tenant.tenantId;
13349
+ let entry = windows.get(key);
13350
+ if (!entry || now - entry.windowStart > windowMs) {
13351
+ entry = { count: 0, windowStart: now };
13352
+ windows.set(key, entry);
13353
+ }
13354
+ entry.count++;
13355
+ if (entry.count > limit) {
13356
+ return c.json({
13357
+ error: "Rate limit exceeded",
13358
+ retryAfterMs: windowMs - (now - entry.windowStart)
13359
+ }, 429);
13360
+ }
13361
+ await next();
13362
+ };
13363
+ }
11966
13364
  // ../mcp/src/client.ts
11967
13365
  import { spawn } from "node:child_process";
11968
13366
  function createMCPClient(config) {
@@ -12174,7 +13572,7 @@ function createMCPClient(config) {
12174
13572
  };
12175
13573
  }
12176
13574
  // ../mcp/src/server.ts
12177
- var log7 = createLogger();
13575
+ var log15 = createLogger();
12178
13576
  function createMCPServer(config) {
12179
13577
  let running = false;
12180
13578
  const toolMap = new Map(config.tools.map((t) => [t.name, t]));
@@ -12309,7 +13707,7 @@ function createMCPServer(config) {
12309
13707
  pendingChunks.shift();
12310
13708
  buffer += chunk;
12311
13709
  if (buffer.length > MAX_BUFFER_SIZE2) {
12312
- log7.error("MCP server: buffer size limit exceeded, resetting");
13710
+ log15.error("MCP server: buffer size limit exceeded, resetting");
12313
13711
  buffer = "";
12314
13712
  continue;
12315
13713
  }
@@ -12344,6 +13742,119 @@ function createMCPServer(config) {
12344
13742
  }
12345
13743
  };
12346
13744
  }
13745
+ // ../client/dist/index.js
13746
+ async function* parseSSEStream(response) {
13747
+ if (!response.body) {
13748
+ throw new Error("Response body is null");
13749
+ }
13750
+ const reader = response.body.getReader();
13751
+ const decoder = new TextDecoder;
13752
+ let buffer = "";
13753
+ try {
13754
+ while (true) {
13755
+ const { done, value } = await reader.read();
13756
+ if (done)
13757
+ break;
13758
+ buffer += decoder.decode(value, { stream: true });
13759
+ const lines = buffer.split(`
13760
+ `);
13761
+ buffer = lines.pop() ?? "";
13762
+ for (const line of lines) {
13763
+ if (line.startsWith("event: error")) {
13764
+ continue;
13765
+ }
13766
+ if (!line.startsWith("data: "))
13767
+ continue;
13768
+ const data = line.slice(6).trim();
13769
+ if (!data || data === "[DONE]")
13770
+ continue;
13771
+ try {
13772
+ const event = JSON.parse(data);
13773
+ yield event;
13774
+ } catch {}
13775
+ }
13776
+ }
13777
+ } finally {
13778
+ reader.releaseLock();
13779
+ }
13780
+ }
13781
+ function createClient(config) {
13782
+ const { baseUrl, apiKey, timeout = 30000 } = config;
13783
+ function headers() {
13784
+ const h = {
13785
+ "Content-Type": "application/json"
13786
+ };
13787
+ if (apiKey) {
13788
+ h.Authorization = `Bearer ${apiKey}`;
13789
+ }
13790
+ return h;
13791
+ }
13792
+ async function request(method, path, body) {
13793
+ const controller = new AbortController;
13794
+ const timer = setTimeout(() => controller.abort(), timeout);
13795
+ try {
13796
+ const response = await fetch(`${baseUrl}${path}`, {
13797
+ method,
13798
+ headers: headers(),
13799
+ body: body ? JSON.stringify(body) : undefined,
13800
+ signal: controller.signal
13801
+ });
13802
+ if (!response.ok) {
13803
+ const errorBody = await response.text().catch(() => "Unknown error");
13804
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
13805
+ }
13806
+ return await response.json();
13807
+ } finally {
13808
+ clearTimeout(timer);
13809
+ }
13810
+ }
13811
+ async function streamRequest(path, body) {
13812
+ const controller = new AbortController;
13813
+ const timer = setTimeout(() => controller.abort(), timeout);
13814
+ try {
13815
+ const response = await fetch(`${baseUrl}${path}`, {
13816
+ method: "POST",
13817
+ headers: headers(),
13818
+ body: JSON.stringify(body),
13819
+ signal: controller.signal
13820
+ });
13821
+ if (!response.ok) {
13822
+ const errorBody = await response.text().catch(() => "Unknown error");
13823
+ clearTimeout(timer);
13824
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
13825
+ }
13826
+ return response;
13827
+ } catch (err2) {
13828
+ clearTimeout(timer);
13829
+ throw err2;
13830
+ }
13831
+ }
13832
+ return {
13833
+ async chat(req) {
13834
+ return request("POST", "/chat", { ...req, stream: false });
13835
+ },
13836
+ async* chatStream(req) {
13837
+ const response = await streamRequest("/chat", { ...req, stream: true });
13838
+ yield* parseSSEStream(response);
13839
+ },
13840
+ async complete(req) {
13841
+ return request("POST", "/complete", { ...req, stream: false });
13842
+ },
13843
+ async* completeStream(req) {
13844
+ const response = await streamRequest("/complete", { ...req, stream: true });
13845
+ yield* parseSSEStream(response);
13846
+ },
13847
+ async health() {
13848
+ return request("GET", "/health");
13849
+ },
13850
+ async metrics() {
13851
+ return request("GET", "/metrics");
13852
+ },
13853
+ async agents() {
13854
+ return request("GET", "/agents");
13855
+ }
13856
+ };
13857
+ }
12347
13858
  // ../testing/src/mock-provider.ts
12348
13859
  function mockProvider(options = {}) {
12349
13860
  const { responses = [], defaultResponse, onRequest } = options;
@@ -12452,10 +13963,10 @@ function mockProvider(options = {}) {
12452
13963
  };
12453
13964
  }
12454
13965
  // ../testing/src/fixtures.ts
12455
- import { createHash } from "node:crypto";
13966
+ import { createHash as createHash3 } from "node:crypto";
12456
13967
  function hashMessages(messages) {
12457
13968
  const content = messages.map((m) => `${m.role}:${m.content}`).join("|");
12458
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
13969
+ return createHash3("sha256").update(content).digest("hex").slice(0, 16);
12459
13970
  }
12460
13971
  function createFixture(name, entries) {
12461
13972
  return {
@@ -12965,7 +14476,7 @@ function createPromptRegistry() {
12965
14476
  };
12966
14477
  }
12967
14478
  // ../testing/src/regression.ts
12968
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14479
+ import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
12969
14480
  import { dirname } from "node:path";
12970
14481
  function makeEmptyResult(name) {
12971
14482
  return {
@@ -13050,7 +14561,7 @@ function createRegressionSuite(name) {
13050
14561
  };
13051
14562
  }
13052
14563
  mkdirSync(dirname(path), { recursive: true });
13053
- writeFileSync(path, JSON.stringify(baseline, null, 2));
14564
+ writeFileSync2(path, JSON.stringify(baseline, null, 2));
13054
14565
  },
13055
14566
  addCase(input, output, score) {
13056
14567
  if (!baseline) {
@@ -13121,7 +14632,9 @@ function createReplayPlayer(entriesOrJson) {
13121
14632
  };
13122
14633
  }
13123
14634
  export {
14635
+ zodToJsonSchema,
13124
14636
  xrayMiddleware,
14637
+ vectorStoreRegistry,
13125
14638
  unwrapOr,
13126
14639
  unwrap,
13127
14640
  tryCatchSync,
@@ -13129,7 +14642,11 @@ export {
13129
14642
  toTraceparent,
13130
14643
  toOTelSpan,
13131
14644
  toOTelExportRequest,
14645
+ tenantRateLimitMiddleware,
14646
+ tenantMiddleware,
14647
+ streamResponse,
13132
14648
  step,
14649
+ sseHeaders,
13133
14650
  sleep,
13134
14651
  securityMiddleware,
13135
14652
  runSupervisor,
@@ -13142,6 +14659,7 @@ export {
13142
14659
  redactSecrets,
13143
14660
  rag,
13144
14661
  parseTraceparent,
14662
+ outputGuardrailMiddleware,
13145
14663
  ok,
13146
14664
  observe,
13147
14665
  mockProvider,
@@ -13157,6 +14675,7 @@ export {
13157
14675
  gateway,
13158
14676
  formatToolResultAsText,
13159
14677
  formatToolResult,
14678
+ formatSSE,
13160
14679
  formatEvalReport,
13161
14680
  extractTraceContext,
13162
14681
  extractText,
@@ -13165,6 +14684,7 @@ export {
13165
14684
  envNumber,
13166
14685
  envBool,
13167
14686
  env,
14687
+ embeddingProviderRegistry,
13168
14688
  detectPromptInjection,
13169
14689
  detectJailbreak,
13170
14690
  defineWorkflow,
@@ -13176,15 +14696,18 @@ export {
13176
14696
  currentTimeTool,
13177
14697
  createToolkit,
13178
14698
  createStream,
14699
+ createSqliteMemoryStore,
13179
14700
  createSpan,
13180
14701
  createSnapshotStore,
13181
14702
  createSemanticValidator,
13182
14703
  createReplayRecorder,
13183
14704
  createReplayPlayer,
13184
14705
  createRegressionSuite,
14706
+ createRegistry,
13185
14707
  createRecorder,
13186
14708
  createProviderMesh,
13187
14709
  createPromptRegistry,
14710
+ createPgVectorStore,
13188
14711
  createOpenAIProvider,
13189
14712
  createOpenAIEmbeddings,
13190
14713
  createOTLPExporter,
@@ -13195,17 +14718,25 @@ export {
13195
14718
  createMCPClient,
13196
14719
  createLogger,
13197
14720
  createInMemoryStore,
14721
+ createInMemoryMemoryStore,
14722
+ createInMemoryCache,
13198
14723
  createGoogleProvider,
13199
14724
  createFixture,
14725
+ createExperiment,
13200
14726
  createCostEngine,
14727
+ createContextManager,
13201
14728
  createConfidenceScorer,
14729
+ createClient,
14730
+ createBatch,
13202
14731
  createApp,
13203
14732
  createAnthropicProvider,
13204
14733
  createAgentSecurity,
14734
+ countTokens,
13205
14735
  costTrackingMiddleware,
13206
14736
  composeMiddleware,
13207
14737
  checkBlockedPatterns,
13208
14738
  calculatorTool,
13209
14739
  calculateCost,
14740
+ cacheMiddleware,
13210
14741
  ElsiumError
13211
14742
  };