elsium-ai 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +465 -81
  2. package/package.json +11 -11
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;
@@ -630,6 +639,101 @@ function createCircuitBreaker(config) {
630
639
  }
631
640
  };
632
641
  }
642
+ // ../core/src/shutdown.ts
643
+ function createShutdownManager(config) {
644
+ const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
645
+ const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
646
+ if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
647
+ throw new ElsiumError({
648
+ code: "CONFIG_ERROR",
649
+ message: "drainTimeoutMs must be >= 0 and finite",
650
+ retryable: false
651
+ });
652
+ }
653
+ let shuttingDown = false;
654
+ let inFlightCount = 0;
655
+ let drainResolve = null;
656
+ let shutdownPromise = null;
657
+ const signalHandlers = [];
658
+ function checkDrained() {
659
+ if (inFlightCount === 0 && drainResolve) {
660
+ drainResolve();
661
+ drainResolve = null;
662
+ }
663
+ }
664
+ async function shutdown() {
665
+ if (shutdownPromise)
666
+ return shutdownPromise;
667
+ shuttingDown = true;
668
+ shutdownPromise = (async () => {
669
+ config?.onDrainStart?.();
670
+ if (inFlightCount === 0) {
671
+ config?.onDrainComplete?.();
672
+ return;
673
+ }
674
+ const drainPromise = new Promise((resolve) => {
675
+ drainResolve = resolve;
676
+ });
677
+ let drainTimer;
678
+ const timeoutPromise = new Promise((resolve) => {
679
+ drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
680
+ });
681
+ const result = await Promise.race([
682
+ drainPromise.then(() => "drained"),
683
+ timeoutPromise
684
+ ]);
685
+ if (drainTimer !== undefined)
686
+ clearTimeout(drainTimer);
687
+ if (result === "timeout") {
688
+ config?.onForceShutdown?.();
689
+ } else {
690
+ config?.onDrainComplete?.();
691
+ }
692
+ })();
693
+ return shutdownPromise;
694
+ }
695
+ const manager = {
696
+ get inFlight() {
697
+ return inFlightCount;
698
+ },
699
+ get isShuttingDown() {
700
+ return shuttingDown;
701
+ },
702
+ async trackOperation(fn) {
703
+ if (shuttingDown) {
704
+ throw new ElsiumError({
705
+ code: "VALIDATION_ERROR",
706
+ message: "Server is shutting down, not accepting new operations",
707
+ retryable: true
708
+ });
709
+ }
710
+ inFlightCount++;
711
+ try {
712
+ return await fn();
713
+ } finally {
714
+ inFlightCount--;
715
+ checkDrained();
716
+ }
717
+ },
718
+ shutdown,
719
+ dispose() {
720
+ for (const { signal, handler } of signalHandlers) {
721
+ process.removeListener(signal, handler);
722
+ }
723
+ signalHandlers.length = 0;
724
+ }
725
+ };
726
+ if (typeof process !== "undefined" && process.on) {
727
+ for (const signal of signals) {
728
+ const handler = () => {
729
+ manager.shutdown();
730
+ };
731
+ signalHandlers.push({ signal, handler });
732
+ process.on(signal, handler);
733
+ }
734
+ }
735
+ return manager;
736
+ }
633
737
  // ../gateway/src/provider.ts
634
738
  var providerRegistry = new Map;
635
739
  var metadataRegistry = new Map;
@@ -1262,7 +1366,19 @@ function createGoogleProvider(config) {
1262
1366
  return { role, parts };
1263
1367
  }
1264
1368
  function formatGeminiMultipartContent(msg, role) {
1265
- const parts = msg.content.filter((p) => p.type === "text").map((p) => ({ text: p.text }));
1369
+ const parts = [];
1370
+ for (const p of msg.content) {
1371
+ if (p.type === "text") {
1372
+ parts.push({ text: p.text });
1373
+ } else if (p.type === "image") {
1374
+ const img = p;
1375
+ if (img.source.type === "base64") {
1376
+ parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
1377
+ } else {
1378
+ parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
1379
+ }
1380
+ }
1381
+ }
1266
1382
  return { role, parts };
1267
1383
  }
1268
1384
  function formatMessages(messages) {
@@ -1437,7 +1553,8 @@ async function handleGoogleErrorResponse(response) {
1437
1553
  throw ElsiumError.authError("google");
1438
1554
  }
1439
1555
  if (response.status === 429) {
1440
- throw ElsiumError.rateLimit("google");
1556
+ const retryAfter = response.headers.get("retry-after");
1557
+ throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
1441
1558
  }
1442
1559
  throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
1443
1560
  provider: "google",
@@ -1623,6 +1740,24 @@ function createOpenAIProvider(config) {
1623
1740
  }
1624
1741
  return openaiMsg;
1625
1742
  }
1743
+ function formatUserContent(msg) {
1744
+ if (typeof msg.content === "string")
1745
+ return msg.content;
1746
+ const parts = [];
1747
+ for (const part of msg.content) {
1748
+ if (part.type === "text") {
1749
+ parts.push({ type: "text", text: part.text });
1750
+ } else if (part.type === "image") {
1751
+ if (part.source.type === "base64") {
1752
+ const url = `data:${part.source.mediaType};base64,${part.source.data}`;
1753
+ parts.push({ type: "image_url", image_url: { url } });
1754
+ } else {
1755
+ parts.push({ type: "image_url", image_url: { url: part.source.url } });
1756
+ }
1757
+ }
1758
+ }
1759
+ return parts;
1760
+ }
1626
1761
  function formatMessages(messages) {
1627
1762
  const formatted = [];
1628
1763
  for (const msg of messages) {
@@ -1638,7 +1773,7 @@ function createOpenAIProvider(config) {
1638
1773
  formatted.push(formatAssistantMessage(msg));
1639
1774
  continue;
1640
1775
  }
1641
- formatted.push({ role: "user", content: extractTextContent(msg) });
1776
+ formatted.push({ role: "user", content: formatUserContent(msg) });
1642
1777
  }
1643
1778
  return formatted;
1644
1779
  }
@@ -1877,7 +2012,7 @@ var PROVIDER_FACTORIES = {
1877
2012
  function registerProviderFactory(name, factory) {
1878
2013
  PROVIDER_FACTORIES[name] = factory;
1879
2014
  }
1880
- function gateway(config) {
2015
+ function validateGatewayConfig(config) {
1881
2016
  const factory = PROVIDER_FACTORIES[config.provider];
1882
2017
  if (!factory) {
1883
2018
  throw new ElsiumError({
@@ -1886,21 +2021,92 @@ function gateway(config) {
1886
2021
  retryable: false
1887
2022
  });
1888
2023
  }
2024
+ if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
2025
+ throw new ElsiumError({
2026
+ code: "CONFIG_ERROR",
2027
+ message: "apiKey must be a non-empty string",
2028
+ retryable: false
2029
+ });
2030
+ }
2031
+ if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
2032
+ throw new ElsiumError({
2033
+ code: "CONFIG_ERROR",
2034
+ message: "timeout must be a positive finite number",
2035
+ retryable: false
2036
+ });
2037
+ }
2038
+ if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
2039
+ throw new ElsiumError({
2040
+ code: "CONFIG_ERROR",
2041
+ message: "maxRetries must be a non-negative finite integer",
2042
+ retryable: false
2043
+ });
2044
+ }
2045
+ return factory;
2046
+ }
2047
+ function autoRegisterProvider(provider) {
2048
+ if (!provider.metadata)
2049
+ return;
2050
+ registerProviderMetadata(provider.name, provider.metadata);
2051
+ if (!provider.metadata.pricing)
2052
+ return;
2053
+ for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
2054
+ registerPricing(model, pricing);
2055
+ }
2056
+ }
2057
+ function validateRequestLimits(request, maxMessages, maxInputTokens) {
2058
+ if (request.messages.length > maxMessages) {
2059
+ throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
2060
+ }
2061
+ let estimatedTokens = 0;
2062
+ for (const msg of request.messages) {
2063
+ const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
2064
+ estimatedTokens += Math.ceil(text.length / 4);
2065
+ }
2066
+ if (estimatedTokens > maxInputTokens) {
2067
+ throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
2068
+ }
2069
+ }
2070
+ function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
2071
+ return {
2072
+ request: req,
2073
+ provider: providerName,
2074
+ model: req.model ?? defaultModel,
2075
+ traceId: generateTraceId(),
2076
+ startTime: performance.now(),
2077
+ metadata
2078
+ };
2079
+ }
2080
+ async function accumulateStreamEvents(stream, emit) {
2081
+ let textContent = "";
2082
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
2083
+ let stopReason = "end_turn";
2084
+ let id = "";
2085
+ for await (const event of stream) {
2086
+ emit(event);
2087
+ if (event.type === "text_delta") {
2088
+ textContent += event.text;
2089
+ } else if (event.type === "message_end") {
2090
+ usage = event.usage;
2091
+ stopReason = event.stopReason;
2092
+ } else if (event.type === "message_start") {
2093
+ id = event.id;
2094
+ }
2095
+ }
2096
+ return { textContent, usage, stopReason, id };
2097
+ }
2098
+ function gateway(config) {
2099
+ const factory = validateGatewayConfig(config);
1889
2100
  const provider = factory({
1890
2101
  apiKey: config.apiKey,
1891
2102
  baseUrl: config.baseUrl,
1892
2103
  timeout: config.timeout,
1893
2104
  maxRetries: config.maxRetries
1894
2105
  });
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
- }
2106
+ autoRegisterProvider(provider);
1903
2107
  const defaultModel = config.model ?? provider.defaultModel;
2108
+ const maxMessages = config.maxMessages ?? 1000;
2109
+ const maxInputTokens = config.maxInputTokens ?? 1e6;
1904
2110
  let xrayStore = null;
1905
2111
  const allMiddleware = [...config.middleware ?? []];
1906
2112
  if (config.xray) {
@@ -1915,14 +2121,7 @@ function gateway(config) {
1915
2121
  if (!composedMiddleware) {
1916
2122
  return provider.complete(req);
1917
2123
  }
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
- };
2124
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1926
2125
  return composedMiddleware(ctx, async (c) => provider.complete(c.request));
1927
2126
  }
1928
2127
  return {
@@ -1934,34 +2133,27 @@ function gateway(config) {
1934
2133
  return xrayStore?.callHistory(limit) ?? [];
1935
2134
  },
1936
2135
  async complete(request) {
2136
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1937
2137
  return executeWithMiddleware(request);
1938
2138
  },
1939
2139
  stream(request) {
2140
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1940
2141
  const req = { ...request, model: request.model ?? defaultModel };
1941
2142
  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
- };
2143
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1950
2144
  return createStream(async (emit) => {
1951
2145
  await composedMiddleware(ctx, async (c) => {
1952
- const stream = provider.stream(c.request);
1953
- for await (const event of stream) {
1954
- emit(event);
1955
- }
2146
+ const result = await accumulateStreamEvents(provider.stream(c.request), emit);
2147
+ const latencyMs = Math.round(performance.now() - ctx.startTime);
1956
2148
  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" },
2149
+ id: result.id,
2150
+ message: { role: "assistant", content: result.textContent },
2151
+ usage: result.usage,
2152
+ cost: calculateCost(c.model, result.usage),
1961
2153
  model: c.model,
1962
2154
  provider: provider.name,
1963
- stopReason: "end_turn",
1964
- latencyMs: 0,
2155
+ stopReason: result.stopReason,
2156
+ latencyMs,
1965
2157
  traceId: ctx.traceId
1966
2158
  };
1967
2159
  });
@@ -2300,6 +2492,12 @@ function redactResponseSecrets(response, config) {
2300
2492
  }
2301
2493
  function securityMiddleware(config) {
2302
2494
  return async (ctx, next) => {
2495
+ if (ctx.request.system) {
2496
+ const sysViolations = scanMessageForViolations(ctx.request.system, config);
2497
+ if (sysViolations.length > 0) {
2498
+ reportAndThrow(sysViolations, config);
2499
+ }
2500
+ }
2303
2501
  for (const message of ctx.request.messages) {
2304
2502
  const text = extractText(message.content);
2305
2503
  if (!text)
@@ -2544,7 +2742,22 @@ function createProviderMesh(config) {
2544
2742
  const available = sortedProviders.find((e) => isProviderAvailable(e.name));
2545
2743
  const entry = available ?? sortedProviders[0];
2546
2744
  const gw = getGateway(entry.name);
2547
- return gw.stream({ ...request, model: request.model ?? entry.model });
2745
+ let resolvedStream = null;
2746
+ callWithCircuitBreaker(entry.name, () => {
2747
+ resolvedStream = gw.stream({ ...request, model: request.model ?? entry.model });
2748
+ return Promise.resolve(resolvedStream);
2749
+ }).catch(() => {});
2750
+ if (resolvedStream === null) {
2751
+ const err2 = new ElsiumError({
2752
+ code: "PROVIDER_ERROR",
2753
+ message: "Circuit breaker is open",
2754
+ retryable: true
2755
+ });
2756
+ return new ElsiumStream(async function* () {
2757
+ yield { type: "error", error: err2 };
2758
+ }());
2759
+ }
2760
+ return resolvedStream;
2548
2761
  }
2549
2762
  };
2550
2763
  }
@@ -2709,6 +2922,18 @@ function zodToJsonSchema(schema) {
2709
2922
  }
2710
2923
  // ../tools/src/toolkit.ts
2711
2924
  function createToolkit(name, tools) {
2925
+ const seen = new Set;
2926
+ for (const tool of tools) {
2927
+ if (seen.has(tool.name)) {
2928
+ throw new ElsiumError({
2929
+ code: "CONFIG_ERROR",
2930
+ message: `Duplicate tool name "${tool.name}" in toolkit "${name}"`,
2931
+ retryable: false,
2932
+ metadata: { toolkit: name, tool: tool.name }
2933
+ });
2934
+ }
2935
+ seen.add(tool.name);
2936
+ }
2712
2937
  const toolMap = new Map(tools.map((t) => [t.name, t]));
2713
2938
  return {
2714
2939
  name,
@@ -6727,7 +6952,26 @@ var coerce = {
6727
6952
  };
6728
6953
  var NEVER = INVALID;
6729
6954
  // ../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;
6955
+ 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;
6956
+ var BLOCKED_HEADER_NAMES = new Set([
6957
+ "cookie",
6958
+ "set-cookie",
6959
+ "authorization",
6960
+ "proxy-authorization",
6961
+ "host",
6962
+ "x-api-key",
6963
+ "x-forwarded-for",
6964
+ "x-real-ip"
6965
+ ]);
6966
+ function sanitizeHeaders(headers) {
6967
+ const safe = {};
6968
+ for (const [key, value] of Object.entries(headers)) {
6969
+ if (!BLOCKED_HEADER_NAMES.has(key.toLowerCase())) {
6970
+ safe[key] = value;
6971
+ }
6972
+ }
6973
+ return safe;
6974
+ }
6731
6975
  function validateUrl(urlString) {
6732
6976
  const parsed = new URL(urlString);
6733
6977
  if (!["http:", "https:"].includes(parsed.protocol)) {
@@ -6752,8 +6996,9 @@ var httpFetchTool = defineTool({
6752
6996
  timeoutMs: 15000,
6753
6997
  handler: async ({ url, headers }, context) => {
6754
6998
  validateUrl(url);
6999
+ const safeHeaders = headers ? sanitizeHeaders(headers) : undefined;
6755
7000
  const response = await fetch(url, {
6756
- headers,
7001
+ headers: safeHeaders,
6757
7002
  signal: context.signal,
6758
7003
  redirect: "manual"
6759
7004
  });
@@ -7421,9 +7666,9 @@ Only respond with JSON, nothing else.`
7421
7666
  if (!parsed) {
7422
7667
  return null;
7423
7668
  }
7424
- const score = parsed.score ?? 0.5;
7669
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
7425
7670
  const threshold = config.hallucination?.threshold ?? 0.7;
7426
- const claims = parsed.hallucinated_claims ?? [];
7671
+ const claims = Array.isArray(parsed.hallucinated_claims) ? parsed.hallucinated_claims : [];
7427
7672
  return {
7428
7673
  passed: score >= threshold,
7429
7674
  score,
@@ -7486,12 +7731,12 @@ Only respond with JSON, nothing else.`
7486
7731
  if (!parsed) {
7487
7732
  return null;
7488
7733
  }
7489
- const score = parsed.score ?? 0.5;
7734
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
7490
7735
  const threshold = config.relevance?.threshold ?? 0.5;
7491
7736
  return {
7492
7737
  passed: score >= threshold,
7493
7738
  score,
7494
- reason: parsed.reason ?? (score >= threshold ? "Output is relevant" : "Output lacks relevance")
7739
+ reason: typeof parsed.reason === "string" ? parsed.reason : score >= threshold ? "Output is relevant" : "Output lacks relevance"
7495
7740
  };
7496
7741
  }
7497
7742
  function checkRelevanceHeuristic(input, output) {
@@ -7543,8 +7788,8 @@ Only respond with JSON, nothing else.`
7543
7788
  if (!parsed) {
7544
7789
  return null;
7545
7790
  }
7546
- const score = parsed.score ?? 0.5;
7547
- const claims = parsed.ungrounded_claims ?? [];
7791
+ const score = typeof parsed.score === "number" ? parsed.score : 0.5;
7792
+ const claims = Array.isArray(parsed.ungrounded_claims) ? parsed.ungrounded_claims : [];
7548
7793
  return {
7549
7794
  passed: score >= 0.7,
7550
7795
  score,
@@ -7605,16 +7850,23 @@ Only respond with JSON, nothing else.`
7605
7850
  }
7606
7851
 
7607
7852
  // ../agents/src/state-machine.ts
7853
+ async function safeHook(fn) {
7854
+ if (!fn)
7855
+ return;
7856
+ try {
7857
+ await fn();
7858
+ } catch (_) {}
7859
+ }
7608
7860
  function executeStateMachine(baseConfig, stateConfig, deps, input, options) {
7609
7861
  return runStateMachine(baseConfig, stateConfig, deps, input, options ?? {});
7610
7862
  }
7611
- function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal) {
7863
+ function handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, signal, hooks, approvalGate, approvalConfig) {
7612
7864
  const toolCalls = response.message.toolCalls;
7613
7865
  if (!toolCalls?.length || response.stopReason !== "tool_use") {
7614
7866
  return null;
7615
7867
  }
7616
7868
  return (async () => {
7617
- const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal);
7869
+ const toolResults = await executeToolCalls(toolCalls, toolMap, toolCallHistory, signal, hooks, approvalGate, approvalConfig);
7618
7870
  const toolMessage = {
7619
7871
  role: "tool",
7620
7872
  content: "",
@@ -7718,6 +7970,7 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7718
7970
  });
7719
7971
  }
7720
7972
  const agentSecurity = baseConfig.guardrails?.security ? createAgentSecurity(baseConfig.guardrails.security) : null;
7973
+ const approvalGate = baseConfig.guardrails?.approval ? createApprovalGate(baseConfig.guardrails.approval) : null;
7721
7974
  const maxTokenBudget = guardrails?.maxTokenBudget ?? 500000;
7722
7975
  const outputValidator = guardrails?.outputValidator ?? (() => true);
7723
7976
  const conversationMessages = [{ role: "user", content: input }];
@@ -7735,7 +7988,8 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7735
7988
  checkTokenBudget(totalInputTokens + totalOutputTokens, maxTokenBudget);
7736
7989
  response = applyOutputGuardrails(response, outputValidator, agentSecurity);
7737
7990
  conversationMessages.push(response.message);
7738
- const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal);
7991
+ await safeHook(() => baseConfig.hooks?.onMessage?.(response.message));
7992
+ const toolCallAction = handleToolCallsAndContinue(response, toolMap, toolCallHistory, conversationMessages, options.signal, baseConfig.hooks, approvalGate, baseConfig.guardrails);
7739
7993
  if (toolCallAction) {
7740
7994
  await toolCallAction;
7741
7995
  continue;
@@ -7756,9 +8010,30 @@ async function runStateMachine(baseConfig, stateConfig, deps, input, options) {
7756
8010
  metadata: { iterations, maxIterations, lastState: currentStateName }
7757
8011
  });
7758
8012
  }
7759
- async function executeToolCalls(toolCalls, toolMap, history, signal) {
8013
+ async function executeToolCalls(toolCalls, toolMap, history, signal, hooks, approvalGate, approvalConfig) {
7760
8014
  const results = [];
7761
8015
  for (const tc of toolCalls) {
8016
+ await safeHook(() => hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
8017
+ if (approvalGate && shouldRequireApproval(approvalConfig?.approval?.requireApprovalFor, {
8018
+ toolName: tc.name
8019
+ })) {
8020
+ const decision = await approvalGate.requestApproval("tool_call", `Execute tool: ${tc.name}`, {
8021
+ toolName: tc.name,
8022
+ arguments: tc.arguments
8023
+ });
8024
+ if (!decision.approved) {
8025
+ const deniedResult = {
8026
+ success: false,
8027
+ error: `Tool call denied: ${decision.reason ?? "Approval denied"}`,
8028
+ toolCallId: tc.id,
8029
+ durationMs: 0
8030
+ };
8031
+ await safeHook(() => hooks?.onToolResult?.(deniedResult));
8032
+ history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
8033
+ results.push(formatToolResult(deniedResult));
8034
+ continue;
8035
+ }
8036
+ }
7762
8037
  const tool = toolMap.get(tc.name);
7763
8038
  if (!tool) {
7764
8039
  const errorResult = {
@@ -7767,11 +8042,13 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
7767
8042
  toolCallId: tc.id,
7768
8043
  durationMs: 0
7769
8044
  };
8045
+ await safeHook(() => hooks?.onToolResult?.(errorResult));
7770
8046
  history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
7771
8047
  results.push(formatToolResult(errorResult));
7772
8048
  continue;
7773
8049
  }
7774
8050
  const result = await tool.execute(tc.arguments, { toolCallId: tc.id, signal });
8051
+ await safeHook(() => hooks?.onToolResult?.(result));
7775
8052
  history.push({ name: tc.name, arguments: tc.arguments, result });
7776
8053
  results.push(formatToolResult(result));
7777
8054
  }
@@ -7779,7 +8056,7 @@ async function executeToolCalls(toolCalls, toolMap, history, signal) {
7779
8056
  }
7780
8057
 
7781
8058
  // ../agents/src/agent.ts
7782
- async function safeHook(fn) {
8059
+ async function safeHook2(fn) {
7783
8060
  if (!fn)
7784
8061
  return;
7785
8062
  try {
@@ -7885,7 +8162,7 @@ function defineAgent(config, deps) {
7885
8162
  traceId,
7886
8163
  confidence
7887
8164
  };
7888
- await safeHook(() => config.hooks?.onComplete?.(agentResult));
8165
+ await safeHook2(() => config.hooks?.onComplete?.(agentResult));
7889
8166
  return { action: "return", result: agentResult };
7890
8167
  }
7891
8168
  function checkBudget(totalInputTokens, totalOutputTokens) {
@@ -7943,7 +8220,7 @@ function defineAgent(config, deps) {
7943
8220
  totalInputTokens += response.usage.inputTokens;
7944
8221
  totalOutputTokens += response.usage.outputTokens;
7945
8222
  totalCost += response.cost.totalCost;
7946
- await safeHook(() => config.hooks?.onMessage?.(response.message));
8223
+ await safeHook2(() => config.hooks?.onMessage?.(response.message));
7947
8224
  conversationMessages.push(response.message);
7948
8225
  if (!response.message.toolCalls?.length || response.stopReason !== "tool_use") {
7949
8226
  const result = await handleNonToolResponse(response, messages, iterations, totalInputTokens, totalOutputTokens, totalCost, toolCallHistory, traceId, conversationMessages);
@@ -7971,7 +8248,7 @@ function defineAgent(config, deps) {
7971
8248
  async function executeToolCalls2(toolCalls, history, options = {}) {
7972
8249
  const results = [];
7973
8250
  for (const tc of toolCalls) {
7974
- await safeHook(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
8251
+ await safeHook2(() => config.hooks?.onToolCall?.({ name: tc.name, arguments: tc.arguments }));
7975
8252
  if (approvalGate && shouldRequireApproval(config.guardrails?.approval?.requireApprovalFor, {
7976
8253
  toolName: tc.name
7977
8254
  })) {
@@ -7983,7 +8260,7 @@ function defineAgent(config, deps) {
7983
8260
  toolCallId: tc.id,
7984
8261
  durationMs: 0
7985
8262
  };
7986
- await safeHook(() => config.hooks?.onToolResult?.(deniedResult));
8263
+ await safeHook2(() => config.hooks?.onToolResult?.(deniedResult));
7987
8264
  history.push({ name: tc.name, arguments: tc.arguments, result: deniedResult });
7988
8265
  results.push(formatToolResult(deniedResult));
7989
8266
  continue;
@@ -7997,7 +8274,7 @@ function defineAgent(config, deps) {
7997
8274
  toolCallId: tc.id,
7998
8275
  durationMs: 0
7999
8276
  };
8000
- await safeHook(() => config.hooks?.onToolResult?.(errorResult));
8277
+ await safeHook2(() => config.hooks?.onToolResult?.(errorResult));
8001
8278
  history.push({ name: tc.name, arguments: tc.arguments, result: errorResult });
8002
8279
  results.push(formatToolResult(errorResult));
8003
8280
  continue;
@@ -8006,7 +8283,7 @@ function defineAgent(config, deps) {
8006
8283
  toolCallId: tc.id,
8007
8284
  signal: options.signal
8008
8285
  });
8009
- await safeHook(() => config.hooks?.onToolResult?.(result));
8286
+ await safeHook2(() => config.hooks?.onToolResult?.(result));
8010
8287
  history.push({ name: tc.name, arguments: tc.arguments, result });
8011
8288
  results.push(formatToolResult(result));
8012
8289
  }
@@ -9267,6 +9544,7 @@ function createCostEngine(config = {}) {
9267
9544
  };
9268
9545
  }
9269
9546
  // ../observe/src/tracer.ts
9547
+ import { writeFileSync } from "node:fs";
9270
9548
  var log3 = createLogger();
9271
9549
  function observe(config = {}) {
9272
9550
  const {
@@ -9282,7 +9560,21 @@ function observe(config = {}) {
9282
9560
  for (const out of output) {
9283
9561
  if (out === "console") {
9284
9562
  handlers.push(consoleHandler);
9285
- } else if (out === "json-file") {} else {
9563
+ } else if (out === "json-file") {
9564
+ exporters.push({
9565
+ name: "json-file",
9566
+ export(spansToExport) {
9567
+ const filename = `.elsium/traces-${Date.now()}.json`;
9568
+ try {
9569
+ writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
9570
+ } catch (err2) {
9571
+ log3.error("Failed to write trace file", {
9572
+ error: err2 instanceof Error ? err2.message : String(err2)
9573
+ });
9574
+ }
9575
+ }
9576
+ });
9577
+ } else {
9286
9578
  exporters.push(out);
9287
9579
  }
9288
9580
  }
@@ -9617,6 +9909,16 @@ function createOTLPExporter(config) {
9617
9909
  } else {
9618
9910
  startAutoFlush();
9619
9911
  }
9912
+ },
9913
+ async shutdown() {
9914
+ if (flushTimer) {
9915
+ clearInterval(flushTimer);
9916
+ flushTimer = null;
9917
+ }
9918
+ if (buffer.length > 0) {
9919
+ const batch = buffer.splice(0, buffer.length);
9920
+ await sendBatch(batch);
9921
+ }
9620
9922
  }
9621
9923
  };
9622
9924
  }
@@ -11692,7 +11994,7 @@ var Hono2 = class extends Hono {
11692
11994
  // ../app/src/middleware.ts
11693
11995
  import { timingSafeEqual } from "node:crypto";
11694
11996
  function corsMiddleware(config = true) {
11695
- const opts = typeof config === "boolean" ? { origin: [], methods: ["GET", "POST", "OPTIONS"] } : config;
11997
+ const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
11696
11998
  return async (c, next) => {
11697
11999
  const requestOrigin = c.req.header("Origin") ?? "";
11698
12000
  let allowedOrigin;
@@ -11769,8 +12071,51 @@ function rateLimitMiddleware(config) {
11769
12071
  await next();
11770
12072
  };
11771
12073
  }
12074
+ function requestIdMiddleware() {
12075
+ return async (c, next) => {
12076
+ const raw2 = c.req.header("X-Request-ID");
12077
+ const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
12078
+ c.set("requestId", id);
12079
+ await next();
12080
+ c.res.headers.set("X-Request-ID", id);
12081
+ };
12082
+ }
12083
+ function requestLoggerMiddleware(logger) {
12084
+ const log5 = logger ?? createLogger();
12085
+ return async (c, next) => {
12086
+ const start = Date.now();
12087
+ await next();
12088
+ const duration = Date.now() - start;
12089
+ log5.info(`${c.req.method} ${c.req.path}`, {
12090
+ method: c.req.method,
12091
+ path: c.req.path,
12092
+ status: c.res.status,
12093
+ durationMs: duration,
12094
+ requestId: c.get("requestId")
12095
+ });
12096
+ };
12097
+ }
11772
12098
 
11773
12099
  // ../app/src/routes.ts
12100
+ function parseJsonBody(raw2) {
12101
+ try {
12102
+ return { ok: true, data: JSON.parse(raw2) };
12103
+ } catch {
12104
+ return { ok: false };
12105
+ }
12106
+ }
12107
+ function elsiumErrorResponse(c, err2, fallbackMessage) {
12108
+ if (err2 instanceof ElsiumError) {
12109
+ return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
12110
+ }
12111
+ return c.json({ error: fallbackMessage }, 500);
12112
+ }
12113
+ function resolveAgent(name, agents, defaultAgent) {
12114
+ const agent = name ? agents.get(name) : defaultAgent;
12115
+ if (agent)
12116
+ return { agent };
12117
+ return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
12118
+ }
11774
12119
  function createRoutes(deps) {
11775
12120
  const app = new Hono2;
11776
12121
  let totalRequests = 0;
@@ -11811,17 +12156,24 @@ function createRoutes(deps) {
11811
12156
  if (rawText.length > MAX_BODY_SIZE) {
11812
12157
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
11813
12158
  }
11814
- const body = JSON.parse(rawText);
12159
+ const parsed = parseJsonBody(rawText);
12160
+ if (!parsed.ok) {
12161
+ return c.json({ error: "Invalid JSON in request body" }, 400);
12162
+ }
12163
+ const body = parsed.data;
11815
12164
  if (!body.message) {
11816
12165
  return c.json({ error: "message is required" }, 400);
11817
12166
  }
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);
12167
+ const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
12168
+ if ("error" in resolved) {
12169
+ return c.json({ error: resolved.error }, 404);
12170
+ }
12171
+ let result;
12172
+ try {
12173
+ result = await resolved.agent.run(body.message);
12174
+ } catch (err2) {
12175
+ return elsiumErrorResponse(c, err2, "Agent execution failed");
11823
12176
  }
11824
- const result = await agent.run(body.message);
11825
12177
  deps.tracer?.trackLLMCall({
11826
12178
  model: "unknown",
11827
12179
  inputTokens: result.usage.totalInputTokens,
@@ -11838,7 +12190,7 @@ function createRoutes(deps) {
11838
12190
  totalTokens: result.usage.totalTokens,
11839
12191
  cost: result.usage.totalCost
11840
12192
  },
11841
- model: agent.config.model ?? "default",
12193
+ model: resolved.agent.config.model ?? "default",
11842
12194
  traceId: result.traceId
11843
12195
  };
11844
12196
  return c.json(response);
@@ -11850,7 +12202,11 @@ function createRoutes(deps) {
11850
12202
  if (rawText.length > MAX_BODY_SIZE) {
11851
12203
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
11852
12204
  }
11853
- const body = JSON.parse(rawText);
12205
+ const parsed = parseJsonBody(rawText);
12206
+ if (!parsed.ok) {
12207
+ return c.json({ error: "Invalid JSON in request body" }, 400);
12208
+ }
12209
+ const body = parsed.data;
11854
12210
  if (!body.messages?.length) {
11855
12211
  return c.json({ error: "messages array is required" }, 400);
11856
12212
  }
@@ -11858,13 +12214,18 @@ function createRoutes(deps) {
11858
12214
  role: m.role,
11859
12215
  content: m.content
11860
12216
  }));
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
- });
12217
+ let response;
12218
+ try {
12219
+ response = await deps.gateway.complete({
12220
+ messages,
12221
+ model: body.model,
12222
+ system: body.system,
12223
+ maxTokens: body.maxTokens,
12224
+ temperature: body.temperature
12225
+ });
12226
+ } catch (err2) {
12227
+ return elsiumErrorResponse(c, err2, "Completion failed");
12228
+ }
11868
12229
  deps.tracer?.trackLLMCall({
11869
12230
  model: response.model,
11870
12231
  inputTokens: response.usage.inputTokens,
@@ -11896,6 +12257,15 @@ function createRoutes(deps) {
11896
12257
  var log5 = createLogger();
11897
12258
  function createApp(config) {
11898
12259
  const app = new Hono2;
12260
+ app.onError((err2, c) => {
12261
+ const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
12262
+ const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
12263
+ log5.error("Unhandled error", { error: err2.message, code, path: c.req.path });
12264
+ return c.json({ error: err2.message, code }, statusCode);
12265
+ });
12266
+ app.notFound((c) => {
12267
+ return c.json({ error: "Not found" }, 404);
12268
+ });
11899
12269
  const providerNames = Object.keys(config.gateway.providers);
11900
12270
  const primaryProvider = providerNames[0];
11901
12271
  const primaryConfig = config.gateway.providers[primaryProvider];
@@ -11910,6 +12280,8 @@ function createApp(config) {
11910
12280
  costTracking: config.observe?.costTracking ?? true
11911
12281
  });
11912
12282
  const serverConfig = config.server ?? {};
12283
+ app.use("*", requestIdMiddleware());
12284
+ app.use("*", requestLoggerMiddleware(log5));
11913
12285
  if (serverConfig.cors) {
11914
12286
  app.use("*", corsMiddleware(serverConfig.cors));
11915
12287
  }
@@ -11932,7 +12304,7 @@ function createApp(config) {
11932
12304
  defaultAgent,
11933
12305
  tracer,
11934
12306
  startTime: Date.now(),
11935
- version: "0.1.0",
12307
+ version: config.version ?? "0.2.2",
11936
12308
  providers: providerNames
11937
12309
  });
11938
12310
  app.route("/", routes);
@@ -11948,13 +12320,25 @@ function createApp(config) {
11948
12320
  port: listenPort,
11949
12321
  hostname
11950
12322
  });
12323
+ let shutdownManager;
12324
+ if (serverConfig.gracefulShutdown) {
12325
+ const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
12326
+ shutdownManager = createShutdownManager({
12327
+ drainTimeoutMs,
12328
+ onDrainStart: () => log5.info("Draining connections..."),
12329
+ onDrainComplete: () => log5.info("Drain complete")
12330
+ });
12331
+ }
11951
12332
  log5.info("ElsiumAI server started", {
11952
12333
  url: `http://${hostname}:${listenPort}`,
11953
12334
  routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
11954
12335
  });
11955
12336
  return {
11956
12337
  port: listenPort,
11957
- stop: () => {
12338
+ stop: async () => {
12339
+ if (shutdownManager) {
12340
+ await shutdownManager.shutdown();
12341
+ }
11958
12342
  server.close();
11959
12343
  }
11960
12344
  };
@@ -12965,7 +13349,7 @@ function createPromptRegistry() {
12965
13349
  };
12966
13350
  }
12967
13351
  // ../testing/src/regression.ts
12968
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
13352
+ import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
12969
13353
  import { dirname } from "node:path";
12970
13354
  function makeEmptyResult(name) {
12971
13355
  return {
@@ -13050,7 +13434,7 @@ function createRegressionSuite(name) {
13050
13434
  };
13051
13435
  }
13052
13436
  mkdirSync(dirname(path), { recursive: true });
13053
- writeFileSync(path, JSON.stringify(baseline, null, 2));
13437
+ writeFileSync2(path, JSON.stringify(baseline, null, 2));
13054
13438
  },
13055
13439
  addCase(input, output, score) {
13056
13440
  if (!baseline) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elsium-ai",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "ElsiumAI — A high-performance, TypeScript-first AI framework",
5
5
  "license": "MIT",
6
6
  "author": "Eric Utrera <ebutrera9103@gmail.com>",
@@ -25,16 +25,16 @@
25
25
  "build": "bun build ./src/index.ts --outdir ./dist --target node && bun x tsc -p tsconfig.build.json --emitDeclarationOnly"
26
26
  },
27
27
  "dependencies": {
28
- "@elsium-ai/core": "^0.2.2",
29
- "@elsium-ai/gateway": "^0.2.2",
30
- "@elsium-ai/agents": "^0.2.2",
31
- "@elsium-ai/tools": "^0.2.2",
32
- "@elsium-ai/rag": "^0.2.2",
33
- "@elsium-ai/workflows": "^0.2.2",
34
- "@elsium-ai/observe": "^0.2.2",
35
- "@elsium-ai/app": "^0.2.2",
36
- "@elsium-ai/testing": "^0.2.2",
37
- "@elsium-ai/mcp": "^0.2.2"
28
+ "@elsium-ai/core": "^0.2.3",
29
+ "@elsium-ai/gateway": "^0.2.3",
30
+ "@elsium-ai/agents": "^0.2.3",
31
+ "@elsium-ai/tools": "^0.2.3",
32
+ "@elsium-ai/rag": "^0.2.3",
33
+ "@elsium-ai/workflows": "^0.2.3",
34
+ "@elsium-ai/observe": "^0.2.3",
35
+ "@elsium-ai/app": "^0.2.3",
36
+ "@elsium-ai/testing": "^0.2.3",
37
+ "@elsium-ai/mcp": "^0.2.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "^5.7.0"