@opensearch-project/agent-health 0.3.0 → 0.4.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.
@@ -593,7 +593,8 @@ var init_claudeCode = __esm({
593
593
  if (s.session_completed) completedSessions++;
594
594
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
595
595
  totalToolCalls += toolCallCount;
596
- const date = s.start_time.slice(0, 10);
596
+ const _d = new Date(s.start_time);
597
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
597
598
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
598
599
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
599
600
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -1487,7 +1488,8 @@ var init_kiro = __esm({
1487
1488
  if (s.session_completed) completedSessions++;
1488
1489
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
1489
1490
  totalToolCalls += toolCallCount;
1490
- const date = s.start_time.slice(0, 10);
1491
+ const _d = new Date(s.start_time);
1492
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
1491
1493
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
1492
1494
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
1493
1495
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -1837,7 +1839,8 @@ var init_codex = __esm({
1837
1839
  if (s.session_completed) completedSessions++;
1838
1840
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
1839
1841
  totalToolCalls += toolCallCount;
1840
- const date = s.start_time.slice(0, 10);
1842
+ const _d = new Date(s.start_time);
1843
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
1841
1844
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
1842
1845
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
1843
1846
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2292,7 +2295,7 @@ function localDateStr(d) {
2292
2295
  function filterByDate(sessions, range) {
2293
2296
  if (!range?.from && !range?.to) return sessions;
2294
2297
  return sessions.filter((s) => {
2295
- const date = s.start_time.slice(0, 10);
2298
+ const date = localDateStr(new Date(s.start_time));
2296
2299
  if (range.from && date < range.from) return false;
2297
2300
  if (range.to && date > range.to) return false;
2298
2301
  return true;
@@ -2318,7 +2321,7 @@ function computeStatsFromSessions(sessions, agent) {
2318
2321
  if (s.session_completed) completedSessions++;
2319
2322
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
2320
2323
  totalToolCalls += toolCallCount;
2321
- const date = s.start_time.slice(0, 10);
2324
+ const date = localDateStr(new Date(s.start_time));
2322
2325
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2323
2326
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
2324
2327
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2496,7 +2499,7 @@ var init_registry = __esm({
2496
2499
  }
2497
2500
  const dailyCostMap = /* @__PURE__ */ new Map();
2498
2501
  for (const s of sessions) {
2499
- const date = s.start_time.slice(0, 10);
2502
+ const date = localDateStr(new Date(s.start_time));
2500
2503
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
2501
2504
  const key = `${date}:${s.agent}`;
2502
2505
  const existing = dailyCostMap.get(key) ?? { date, cost: 0, agent: s.agent };
@@ -2524,7 +2527,7 @@ var init_registry = __esm({
2524
2527
  if (!s.start_time) continue;
2525
2528
  const d = new Date(s.start_time);
2526
2529
  if (isNaN(d.getTime())) continue;
2527
- activeDates.add(s.start_time.slice(0, 10));
2530
+ activeDates.add(localDateStr(new Date(s.start_time)));
2528
2531
  dowCounts[d.getDay()]++;
2529
2532
  hourCounts[d.getHours()]++;
2530
2533
  }
@@ -2551,7 +2554,7 @@ var init_registry = __esm({
2551
2554
  }
2552
2555
  const dailyMap = /* @__PURE__ */ new Map();
2553
2556
  for (const s of sessions) {
2554
- const date = s.start_time.slice(0, 10);
2557
+ const date = localDateStr(new Date(s.start_time));
2555
2558
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
2556
2559
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
2557
2560
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2670,7 +2673,7 @@ var init_registry = __esm({
2670
2673
  const totalDuration = data.sessions.reduce((s, sess) => s + sess.duration_minutes, 0);
2671
2674
  const dailyCostMap = /* @__PURE__ */ new Map();
2672
2675
  for (const s of data.sessions) {
2673
- const date = s.start_time.slice(0, 10);
2676
+ const date = localDateStr(new Date(s.start_time));
2674
2677
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
2675
2678
  const key = `${date}:${s.agent}`;
2676
2679
  const existing = dailyCostMap.get(key) ?? { date, cost: 0, agent: s.agent };
@@ -2879,7 +2882,7 @@ var init_registry = __esm({
2879
2882
  const totalToolErrors = userSessions.reduce((s, sess) => s + sess.total_tool_errors, 0);
2880
2883
  const totalDuration = userSessions.reduce((s, sess) => s + sess.duration_minutes, 0);
2881
2884
  const agents = [...new Set(userSessions.map((s) => s.agent))];
2882
- const activeDates = new Set(userSessions.map((s) => s.start_time.slice(0, 10)));
2885
+ const activeDates = new Set(userSessions.map((s) => localDateStr(new Date(s.start_time))));
2883
2886
  const projectCounts = /* @__PURE__ */ new Map();
2884
2887
  for (const s of userSessions) {
2885
2888
  projectCounts.set(s.project_path, (projectCounts.get(s.project_path) ?? 0) + 1);
@@ -3011,9 +3014,10 @@ var init_remoteAggregator = __esm({
3011
3014
  filterByRange(sessions, range) {
3012
3015
  if (!range?.from && !range?.to) return sessions;
3013
3016
  return sessions.filter((s) => {
3014
- const t = s.start_time;
3015
- if (range.from && t < range.from) return false;
3016
- if (range.to && t > range.to + "T23:59:59") return false;
3017
+ const y = new Date(s.start_time);
3018
+ const date = `${y.getFullYear()}-${String(y.getMonth() + 1).padStart(2, "0")}-${String(y.getDate()).padStart(2, "0")}`;
3019
+ if (range.from && date < range.from) return false;
3020
+ if (range.to && date > range.to) return false;
3017
3021
  return true;
3018
3022
  });
3019
3023
  }
@@ -3161,6 +3165,8 @@ var init_config = __esm({
3161
3165
  // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
3162
3166
  openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
3163
3167
  openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
3168
+ // Observio Sample Agent endpoint
3169
+ observioEndpoint: getEnvVar("OBSERVIO_ENDPOINT", "http://localhost:3001/run-agent"),
3164
3170
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
3165
3171
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
3166
3172
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -3225,6 +3231,16 @@ var init_constants = __esm({
3225
3231
  description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
3226
3232
  serverOnly: true
3227
3233
  },
3234
+ "strands": {
3235
+ label: "Amazon Strands",
3236
+ description: "Amazon Strands agent framework via Bedrock Agent Runtime API. Server-only \u2014 requires AWS SDK.",
3237
+ serverOnly: true
3238
+ },
3239
+ "langgraph": {
3240
+ label: "LangGraph (REST)",
3241
+ description: "LangGraph agent via direct REST API. Use for non-AG-UI LangGraph instances.",
3242
+ serverOnly: false
3243
+ },
3228
3244
  "mock": {
3229
3245
  label: "Mock",
3230
3246
  description: "Built-in demo agent for testing. No real endpoint needed.",
@@ -3244,6 +3260,15 @@ var init_constants = __esm({
3244
3260
  headers: {},
3245
3261
  useTraces: false
3246
3262
  },
3263
+ {
3264
+ key: "observio",
3265
+ name: "Observio Sample Agent",
3266
+ endpoint: ENV_CONFIG.observioEndpoint || "http://localhost:3001/run-agent",
3267
+ description: "Observio sample agent \u2014 ReAct pattern with LangGraph and Bedrock. Start with: cd observio-sample-agent && npm run start:ag-ui",
3268
+ connectorType: "agui-streaming",
3269
+ headers: {},
3270
+ useTraces: true
3271
+ },
3247
3272
  {
3248
3273
  key: "claude-code",
3249
3274
  name: "Claude Code",
@@ -3253,6 +3278,30 @@ var init_constants = __esm({
3253
3278
  headers: {},
3254
3279
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
3255
3280
  connectorConfig: { env: getClaudeCodeConnectorEnv() }
3281
+ },
3282
+ {
3283
+ key: "strands",
3284
+ name: "Amazon Strands",
3285
+ endpoint: "${STRANDS_AGENT_ID}",
3286
+ description: "Amazon Strands agent framework (Bedrock Agent Runtime)",
3287
+ connectorType: "strands",
3288
+ headers: {},
3289
+ useTraces: false,
3290
+ connectorConfig: {
3291
+ agentAliasId: "${STRANDS_ALIAS_ID:-TSTALIASID}",
3292
+ region: "${AWS_REGION:-us-east-1}"
3293
+ },
3294
+ enabled: false
3295
+ },
3296
+ {
3297
+ key: "langgraph-rest",
3298
+ name: "LangGraph (REST)",
3299
+ endpoint: "${LANGGRAPH_API_ENDPOINT:-http://localhost:8000}",
3300
+ description: "LangGraph agent via direct REST API",
3301
+ connectorType: "langgraph",
3302
+ headers: {},
3303
+ useTraces: false,
3304
+ enabled: false
3256
3305
  }
3257
3306
  ],
3258
3307
  models: {
@@ -3436,6 +3485,7 @@ function mergeConfigs(userConfig, defaultConfig) {
3436
3485
  ...DEFAULT_SERVER_CONFIG,
3437
3486
  ...userConfig.server
3438
3487
  };
3488
+ const telemetry = userConfig.telemetry ?? {};
3439
3489
  return {
3440
3490
  server,
3441
3491
  agents,
@@ -3443,7 +3493,8 @@ function mergeConfigs(userConfig, defaultConfig) {
3443
3493
  connectors,
3444
3494
  testCases,
3445
3495
  reporters,
3446
- judge
3496
+ judge,
3497
+ telemetry
3447
3498
  };
3448
3499
  }
3449
3500
  async function loadUserConfig(configPath) {
@@ -3485,7 +3536,8 @@ function loadConfigSync(cwd = process.cwd()) {
3485
3536
  connectors: [],
3486
3537
  testCases: [],
3487
3538
  reporters: [["console"]],
3488
- judge: { provider: "bedrock", model: "claude-sonnet-4" }
3539
+ judge: { provider: "bedrock", model: "claude-sonnet-4" },
3540
+ telemetry: {}
3489
3541
  };
3490
3542
  }
3491
3543
  function clearConfigCache() {
@@ -4186,15 +4238,15 @@ router2.post("/api/judge", async (req, res) => {
4186
4238
  if (!modelConfig) {
4187
4239
  modelConfig = Object.values(config2.models).find((m) => m.model_id === modelId);
4188
4240
  }
4189
- const provider = modelConfig?.provider || "bedrock";
4241
+ const provider2 = modelConfig?.provider || "bedrock";
4190
4242
  const resolvedModelId = modelConfig?.model_id || modelId;
4191
- debug("JudgeAPI", "Using provider:", provider, "model:", resolvedModelId);
4192
- if (provider === "demo") {
4243
+ debug("JudgeAPI", "Using provider:", provider2, "model:", resolvedModelId);
4244
+ if (provider2 === "demo") {
4193
4245
  debug("JudgeAPI", "Demo provider - returning mock evaluation");
4194
4246
  const mockResult = generateMockEvaluation(trajectory, expectedOutcomes);
4195
4247
  return res.json(mockResult);
4196
4248
  }
4197
- if (provider === "openai-compatible") {
4249
+ if (provider2 === "openai-compatible") {
4198
4250
  debug("JudgeAPI", "OpenAI-compatible provider - calling endpoint");
4199
4251
  const result2 = await evaluateWithOpenAICompatible(
4200
4252
  { trajectory, expectedOutcomes, expectedTrajectory, logs },
@@ -4211,7 +4263,7 @@ router2.post("/api/judge", async (req, res) => {
4211
4263
  res.json(result);
4212
4264
  } catch (error) {
4213
4265
  console.error("[JudgeAPI] Error during evaluation:", error);
4214
- const provider = (() => {
4266
+ const provider2 = (() => {
4215
4267
  try {
4216
4268
  const config2 = loadConfigSync();
4217
4269
  const { modelId } = req.body;
@@ -4221,7 +4273,7 @@ router2.post("/api/judge", async (req, res) => {
4221
4273
  return "bedrock";
4222
4274
  }
4223
4275
  })();
4224
- const errorMessage = provider === "openai-compatible" ? parseOpenAICompatibleError(error) : parseBedrockError(error);
4276
+ const errorMessage = provider2 === "openai-compatible" ? parseOpenAICompatibleError(error) : parseBedrockError(error);
4225
4277
  res.status(500).json({
4226
4278
  error: `Judge evaluation failed: ${errorMessage}`,
4227
4279
  details: error.message
@@ -4512,13 +4564,13 @@ function getCustomAgents() {
4512
4564
  }
4513
4565
 
4514
4566
  // lib/hooks.ts
4515
- async function executeBeforeRequestHook(hooks, context, agentKey) {
4567
+ async function executeBeforeRequestHook(hooks, context2, agentKey) {
4516
4568
  if (!hooks?.beforeRequest) {
4517
- return context;
4569
+ return context2;
4518
4570
  }
4519
4571
  debug("Hooks", `Executing beforeRequest hook for agent "${agentKey}"`);
4520
4572
  try {
4521
- const result = await hooks.beforeRequest(context);
4573
+ const result = await hooks.beforeRequest(context2);
4522
4574
  if (!result || typeof result !== "object") {
4523
4575
  throw new Error("beforeRequest hook must return an object with endpoint, payload, and headers");
4524
4576
  }
@@ -4538,12 +4590,12 @@ async function executeBeforeRequestHook(hooks, context, agentKey) {
4538
4590
  throw new Error(`beforeRequest hook failed for agent "${agentKey}": ${message}`);
4539
4591
  }
4540
4592
  }
4541
- async function executeAfterResponseHook(hooks, context, agentKey) {
4593
+ async function executeAfterResponseHook(hooks, context2, agentKey) {
4542
4594
  if (!hooks?.afterResponse) {
4543
- return context;
4595
+ return context2;
4544
4596
  }
4545
4597
  try {
4546
- const result = await hooks.afterResponse(context);
4598
+ const result = await hooks.afterResponse(context2);
4547
4599
  if (!result || typeof result !== "object") {
4548
4600
  throw new Error("afterResponse hook must return an object with response, trajectory, and optional runId");
4549
4601
  }
@@ -4559,12 +4611,12 @@ async function executeAfterResponseHook(hooks, context, agentKey) {
4559
4611
  throw new Error(`afterResponse hook failed for agent "${agentKey}": ${message}`);
4560
4612
  }
4561
4613
  }
4562
- async function executeBuildTrajectoryHook(hooks, context, agentKey) {
4614
+ async function executeBuildTrajectoryHook(hooks, context2, agentKey) {
4563
4615
  if (!hooks?.buildTrajectory) {
4564
4616
  return null;
4565
4617
  }
4566
4618
  try {
4567
- const result = await hooks.buildTrajectory(context);
4619
+ const result = await hooks.buildTrajectory(context2);
4568
4620
  if (result !== null && !Array.isArray(result)) {
4569
4621
  throw new Error("buildTrajectory hook must return an array of TrajectoryStep objects or null");
4570
4622
  }
@@ -4615,6 +4667,36 @@ var agent_default = router3;
4615
4667
  import { Router as Router4 } from "express";
4616
4668
 
4617
4669
  // server/services/tracesService.ts
4670
+ function classifyOpenSearchError(error) {
4671
+ const statusCode = error.meta?.statusCode;
4672
+ const message = error.message || "Unknown error";
4673
+ if (statusCode === 401 || statusCode === 403) {
4674
+ return {
4675
+ category: "auth",
4676
+ message: `Authentication failed (HTTP ${statusCode})`,
4677
+ suggestion: "AWS credentials may be expired or invalid. Run `aws sts get-caller-identity` to verify, then refresh with `aws sso login` or update your credentials."
4678
+ };
4679
+ }
4680
+ if (statusCode === 404) {
4681
+ return {
4682
+ category: "index_not_found",
4683
+ message: "Trace index not found",
4684
+ suggestion: "The OpenSearch traces index does not exist yet. Ensure your agents are configured to emit OpenTelemetry spans."
4685
+ };
4686
+ }
4687
+ if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("getaddrinfo") || message.includes("Connection refused")) {
4688
+ return {
4689
+ category: "connection",
4690
+ message: `Cannot connect to OpenSearch: ${message}`,
4691
+ suggestion: "Check that the OpenSearch endpoint is correct and the cluster is reachable from your network."
4692
+ };
4693
+ }
4694
+ return {
4695
+ category: "unknown",
4696
+ message,
4697
+ suggestion: "Check the server logs for more details."
4698
+ };
4699
+ }
4618
4700
  function transformSpan(source) {
4619
4701
  const attributes = {};
4620
4702
  for (const [key, value] of Object.entries(source)) {
@@ -4669,11 +4751,11 @@ function transformSpan(source) {
4669
4751
  };
4670
4752
  }
4671
4753
  async function fetchTraces(options, client, indexPattern = "otel-v1-apm-span-*") {
4672
- const { traceId, runIds, startTime, endTime, size = 100, serviceName, textSearch, cursor } = options;
4754
+ const { traceId, runIds, sessionId, startTime, endTime, size = 100, serviceName, textSearch, cursor } = options;
4673
4755
  const hasTimeRange = startTime || endTime;
4674
- const hasIdFilter = traceId || runIds && runIds.length > 0;
4756
+ const hasIdFilter = traceId || runIds && runIds.length > 0 || sessionId;
4675
4757
  if (!hasIdFilter && !hasTimeRange) {
4676
- throw new Error("Either traceId, runIds, or time range is required");
4758
+ throw new Error("Either traceId, runIds, sessionId, or time range is required");
4677
4759
  }
4678
4760
  debug("TracesService", "Fetching traces:", { traceId, runIds: runIds?.length, serviceName, textSearch, size, cursor: cursor ? "present" : "none" });
4679
4761
  const must = [];
@@ -4685,6 +4767,9 @@ async function fetchTraces(options, client, indexPattern = "otel-v1-apm-span-*")
4685
4767
  terms: { "span.attributes.gen_ai@request@id": runIds }
4686
4768
  });
4687
4769
  }
4770
+ if (sessionId) {
4771
+ must.push({ term: { "span.attributes.session@id": sessionId } });
4772
+ }
4688
4773
  if (startTime || endTime) {
4689
4774
  const range = { "startTime": {} };
4690
4775
  if (startTime) range["startTime"].gte = new Date(startTime).toISOString();
@@ -4756,7 +4841,13 @@ async function checkTracesHealth(client, indexPattern = "otel-v1-apm-span-*") {
4756
4841
  return { status: "error", index: indexPattern };
4757
4842
  }
4758
4843
  } catch (error) {
4759
- return { status: "error", error: error.message };
4844
+ const classified = classifyOpenSearchError(error);
4845
+ return {
4846
+ status: "error",
4847
+ error: classified.message,
4848
+ errorCategory: classified.category,
4849
+ suggestion: classified.suggestion
4850
+ };
4760
4851
  }
4761
4852
  }
4762
4853
 
@@ -6410,10 +6501,10 @@ function createOpenSearchClient(config2) {
6410
6501
  region: config2.awsRegion,
6411
6502
  service: config2.awsService || "es",
6412
6503
  getCredentials: () => {
6413
- const provider = fromNodeProviderChain2({
6504
+ const provider2 = fromNodeProviderChain2({
6414
6505
  ...config2.awsProfile && { profile: config2.awsProfile }
6415
6506
  });
6416
- return provider();
6507
+ return provider2();
6417
6508
  }
6418
6509
  });
6419
6510
  return new Client({
@@ -7334,15 +7425,19 @@ var OpenSearchRunOperations = class {
7334
7425
  async getAll(options) {
7335
7426
  const size = options?.size ?? 100;
7336
7427
  const from = options?.from ?? 0;
7428
+ const body = {
7429
+ size,
7430
+ from,
7431
+ sort: [{ createdAt: { order: "desc" } }],
7432
+ query: { match_all: {} }
7433
+ };
7434
+ if (options?._source && options._source.length > 0) {
7435
+ body._source = options._source;
7436
+ }
7337
7437
  try {
7338
7438
  const result = await this.client.search({
7339
7439
  index: this.index,
7340
- body: {
7341
- size,
7342
- from,
7343
- sort: [{ createdAt: { order: "desc" } }],
7344
- query: { match_all: {} }
7345
- }
7440
+ body
7346
7441
  });
7347
7442
  const items = hitsToSources(result.body.hits?.hits || []);
7348
7443
  const total = typeof result.body.hits?.total === "object" ? result.body.hits.total.value : result.body.hits?.total ?? 0;
@@ -8046,8 +8141,12 @@ function resolveObservabilityConfig(req) {
8046
8141
  if (fileConfig) {
8047
8142
  return {
8048
8143
  endpoint: fileConfig.endpoint,
8144
+ authType: fileConfig.authType,
8049
8145
  username: fileConfig.username,
8050
8146
  password: fileConfig.password,
8147
+ awsProfile: fileConfig.awsProfile,
8148
+ awsRegion: fileConfig.awsRegion,
8149
+ awsService: fileConfig.awsService,
8051
8150
  tlsSkipVerify: fileConfig.tlsSkipVerify,
8052
8151
  indexes: {
8053
8152
  traces: fileConfig.indexes?.traces || DEFAULT_OTEL_INDEXES.traces,
@@ -8091,14 +8190,34 @@ function getStorageConfigFromEnv() {
8091
8190
  awsService: process.env.OPENSEARCH_STORAGE_AWS_SERVICE || void 0
8092
8191
  };
8093
8192
  }
8193
+ function getObservabilityConfigFromEnv() {
8194
+ const endpoint = process.env.OPENSEARCH_LOGS_ENDPOINT;
8195
+ if (!endpoint) {
8196
+ return null;
8197
+ }
8198
+ return {
8199
+ endpoint,
8200
+ authType: process.env.OPENSEARCH_LOGS_AUTH_TYPE || void 0,
8201
+ username: process.env.OPENSEARCH_LOGS_USERNAME,
8202
+ password: process.env.OPENSEARCH_LOGS_PASSWORD,
8203
+ awsProfile: process.env.OPENSEARCH_LOGS_AWS_PROFILE,
8204
+ awsRegion: process.env.OPENSEARCH_LOGS_AWS_REGION,
8205
+ awsService: process.env.OPENSEARCH_LOGS_AWS_SERVICE || void 0,
8206
+ indexes: {
8207
+ traces: process.env.OPENSEARCH_LOGS_TRACES_INDEX || DEFAULT_OTEL_INDEXES.traces,
8208
+ logs: process.env.OPENSEARCH_LOGS_INDEX || DEFAULT_OTEL_INDEXES.logs,
8209
+ metrics: DEFAULT_OTEL_INDEXES.metrics
8210
+ }
8211
+ };
8212
+ }
8094
8213
 
8095
8214
  // server/routes/traces.ts
8096
8215
  var router4 = Router4();
8097
8216
  router4.post("/api/traces", async (req, res) => {
8098
8217
  try {
8099
- const { traceId, runIds, startTime, endTime, size = 100, serviceName, textSearch, cursor } = req.body;
8218
+ const { traceId, runIds, sessionId, startTime, endTime, size = 100, serviceName, textSearch, cursor } = req.body;
8100
8219
  const hasTimeRange = startTime || endTime;
8101
- const hasIdFilter = traceId || runIds && runIds.length > 0;
8220
+ const hasIdFilter = traceId || runIds && runIds.length > 0 || sessionId;
8102
8221
  if (!hasIdFilter && !hasTimeRange) {
8103
8222
  return res.status(400).json({
8104
8223
  error: "Either traceId, runIds, or time range is required"
@@ -8112,16 +8231,18 @@ router4.post("/api/traces", async (req, res) => {
8112
8231
  }
8113
8232
  let realSpans = [];
8114
8233
  let warning;
8234
+ let warningCategory;
8235
+ let suggestion;
8115
8236
  let nextCursor = null;
8116
8237
  let hasMore = false;
8117
8238
  const config2 = resolveObservabilityConfig(req);
8118
- if (config2 && (traceId || runIds && runIds.length > 0 || startTime || endTime)) {
8239
+ if (config2 && (traceId || runIds && runIds.length > 0 || sessionId || startTime || endTime)) {
8119
8240
  let client;
8120
8241
  try {
8121
8242
  client = createOpenSearchClient(config2);
8122
8243
  const indexPattern = config2.indexes?.traces || DEFAULT_OTEL_INDEXES.traces;
8123
8244
  const result = await fetchTraces(
8124
- { traceId, runIds, startTime, endTime, size, serviceName, textSearch, cursor },
8245
+ { traceId, runIds, sessionId, startTime, endTime, size, serviceName, textSearch, cursor },
8125
8246
  client,
8126
8247
  indexPattern
8127
8248
  );
@@ -8129,8 +8250,11 @@ router4.post("/api/traces", async (req, res) => {
8129
8250
  nextCursor = result.nextCursor || null;
8130
8251
  hasMore = result.hasMore || false;
8131
8252
  } catch (e) {
8132
- console.warn("[TracesAPI] OpenSearch query failed:", e.message);
8133
- warning = e.message;
8253
+ const classified = classifyOpenSearchError(e);
8254
+ console.warn(`[TracesAPI] OpenSearch query failed (${classified.category}):`, classified.message);
8255
+ warning = classified.message;
8256
+ warningCategory = classified.category;
8257
+ suggestion = classified.suggestion;
8134
8258
  } finally {
8135
8259
  if (client) {
8136
8260
  await client.close().catch(() => {
@@ -8156,6 +8280,8 @@ router4.post("/api/traces", async (req, res) => {
8156
8280
  }
8157
8281
  }
8158
8282
  warning = "Observability data source not configured";
8283
+ warningCategory = "not_configured";
8284
+ suggestion = "Configure OPENSEARCH_LOGS_ENDPOINT or observabilityStorage in agent-health.config.ts to connect to your traces cluster.";
8159
8285
  }
8160
8286
  const allSpans = [...sampleSpans, ...realSpans];
8161
8287
  res.json({
@@ -8163,7 +8289,9 @@ router4.post("/api/traces", async (req, res) => {
8163
8289
  total: allSpans.length,
8164
8290
  nextCursor,
8165
8291
  hasMore,
8166
- warning
8292
+ warning,
8293
+ warningCategory,
8294
+ suggestion
8167
8295
  });
8168
8296
  } catch (error) {
8169
8297
  console.error("[TracesAPI] Error:", error);
@@ -8280,33 +8408,21 @@ function computeMetricsFromSampleSpans(runId) {
8280
8408
  status: "success"
8281
8409
  };
8282
8410
  }
8283
- async function computeMetrics(runId, osConfig) {
8284
- const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8285
- const query = {
8286
- size: 500,
8287
- sort: [{ startTime: { order: "asc" } }],
8288
- query: {
8289
- bool: {
8290
- must: [
8291
- { term: { "span.attributes.gen_ai@request@id": runId } }
8292
- ]
8293
- }
8294
- }
8295
- };
8296
- const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8297
- method: "POST",
8298
- headers: {
8299
- "Content-Type": "application/json",
8300
- "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8301
- },
8302
- body: JSON.stringify(query)
8303
- });
8304
- if (!response.ok) {
8305
- const errorText = await response.text();
8306
- throw new Error(`OpenSearch query failed: ${response.status} - ${errorText}`);
8307
- }
8308
- const data = await response.json();
8309
- const spans = data.hits?.hits?.map((h) => h._source) || [];
8411
+ var METRICS_SOURCE_FIELDS = [
8412
+ "span.attributes.gen_ai@request@id",
8413
+ "span.attributes.gen_ai@usage@input_tokens",
8414
+ "span.attributes.gen_ai@usage@output_tokens",
8415
+ "span.attributes.gen_ai@request@model",
8416
+ "span.attributes.gen_ai@tool@name",
8417
+ "span.attributes.tool.name",
8418
+ "name",
8419
+ "traceId",
8420
+ "startTime",
8421
+ "endTime",
8422
+ "durationInNanos",
8423
+ "status.code"
8424
+ ];
8425
+ function computeMetricsFromSpans(runId, spans) {
8310
8426
  if (spans.length === 0) {
8311
8427
  return {
8312
8428
  runId,
@@ -8378,6 +8494,95 @@ async function computeMetrics(runId, osConfig) {
8378
8494
  status
8379
8495
  };
8380
8496
  }
8497
+ async function computeMetrics(runId, osConfig) {
8498
+ const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8499
+ const query = {
8500
+ size: 500,
8501
+ sort: [{ startTime: { order: "asc" } }],
8502
+ query: {
8503
+ bool: {
8504
+ must: [
8505
+ { term: { "span.attributes.gen_ai@request@id": runId } }
8506
+ ]
8507
+ }
8508
+ }
8509
+ };
8510
+ const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8511
+ method: "POST",
8512
+ headers: {
8513
+ "Content-Type": "application/json",
8514
+ "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8515
+ },
8516
+ body: JSON.stringify(query)
8517
+ });
8518
+ if (!response.ok) {
8519
+ const errorText = await response.text();
8520
+ throw new Error(`OpenSearch query failed: ${response.status} - ${errorText}`);
8521
+ }
8522
+ const data = await response.json();
8523
+ const spans = data.hits?.hits?.map((h) => h._source) || [];
8524
+ return computeMetricsFromSpans(runId, spans);
8525
+ }
8526
+ async function computeBatchMetrics(runIds, osConfig) {
8527
+ if (runIds.length === 0) return [];
8528
+ const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8529
+ const CHUNK_SIZE = 50;
8530
+ const allResults = [];
8531
+ const chunks = [];
8532
+ for (let i = 0; i < runIds.length; i += CHUNK_SIZE) {
8533
+ chunks.push(runIds.slice(i, i + CHUNK_SIZE));
8534
+ }
8535
+ const chunkResults = await Promise.all(chunks.map(async (chunk) => {
8536
+ const query = {
8537
+ size: 1e4,
8538
+ sort: [{ startTime: { order: "asc" } }],
8539
+ _source: METRICS_SOURCE_FIELDS,
8540
+ query: {
8541
+ bool: {
8542
+ must: [
8543
+ { terms: { "span.attributes.gen_ai@request@id": chunk } }
8544
+ ]
8545
+ }
8546
+ }
8547
+ };
8548
+ const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8549
+ method: "POST",
8550
+ headers: {
8551
+ "Content-Type": "application/json",
8552
+ "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8553
+ },
8554
+ body: JSON.stringify(query)
8555
+ });
8556
+ if (!response.ok) {
8557
+ const responseBody = await response.text();
8558
+ console.warn(
8559
+ `OpenSearch metrics query failed for chunk (${chunk.length} run IDs): ${response.status} ${response.statusText}. Response body: ${responseBody}`
8560
+ );
8561
+ return chunk.map((runId) => computeMetricsFromSpans(runId, []));
8562
+ }
8563
+ const data = await response.json();
8564
+ const allSpans = data.hits?.hits?.map((h) => h._source) || [];
8565
+ const totalHits = data.hits?.total?.value ?? allSpans.length;
8566
+ if (totalHits > 1e4) {
8567
+ console.warn(
8568
+ `OpenSearch batch metrics query returned ${allSpans.length} of ${totalHits} spans for chunk of ${chunk.length} run IDs. Metrics may be incomplete.`
8569
+ );
8570
+ }
8571
+ const spansByRunId = /* @__PURE__ */ new Map();
8572
+ for (const rid of chunk) spansByRunId.set(rid, []);
8573
+ for (const span of allSpans) {
8574
+ const rid = span["span.attributes.gen_ai@request@id"];
8575
+ if (rid && spansByRunId.has(rid)) {
8576
+ spansByRunId.get(rid).push(span);
8577
+ }
8578
+ }
8579
+ return chunk.map((runId) => computeMetricsFromSpans(runId, spansByRunId.get(runId) || []));
8580
+ }));
8581
+ for (const results of chunkResults) {
8582
+ allResults.push(...results);
8583
+ }
8584
+ return allResults;
8585
+ }
8381
8586
  function computeAggregateMetrics(metricsArray) {
8382
8587
  if (!metricsArray || metricsArray.length === 0) {
8383
8588
  return {
@@ -8456,15 +8661,6 @@ router5.get("/api/metrics/:runId", async (req, res) => {
8456
8661
  res.status(500).json({ error: error.message });
8457
8662
  }
8458
8663
  });
8459
- async function processBatches(items, batchSize, processor) {
8460
- const results = [];
8461
- for (let i = 0; i < items.length; i += batchSize) {
8462
- const batch = items.slice(i, i + batchSize);
8463
- const batchResults = await Promise.all(batch.map(processor));
8464
- results.push(...batchResults);
8465
- }
8466
- return results;
8467
- }
8468
8664
  router5.post("/api/metrics/batch", async (req, res) => {
8469
8665
  try {
8470
8666
  const { runIds } = req.body;
@@ -8495,16 +8691,15 @@ router5.post("/api/metrics/batch", async (req, res) => {
8495
8691
  password: config2.password,
8496
8692
  indexPattern
8497
8693
  };
8498
- const BATCH_SIZE = 10;
8499
- realResults = await processBatches(
8500
- realRunIds,
8501
- BATCH_SIZE,
8502
- (runId) => computeMetrics(runId, osConfig).catch((e) => ({
8694
+ try {
8695
+ realResults = await computeBatchMetrics(realRunIds, osConfig);
8696
+ } catch (e) {
8697
+ realResults = realRunIds.map((runId) => ({
8503
8698
  runId,
8504
8699
  error: e.message,
8505
8700
  status: "error"
8506
- }))
8507
- );
8701
+ }));
8702
+ }
8508
8703
  }
8509
8704
  }
8510
8705
  const resultsMap = /* @__PURE__ */ new Map();
@@ -12174,11 +12369,150 @@ var OpenAICompatibleConnector = class extends BaseConnector {
12174
12369
  };
12175
12370
  var openaiCompatibleConnector = new OpenAICompatibleConnector();
12176
12371
 
12372
+ // services/connectors/langgraph/LangGraphConnector.ts
12373
+ var LangGraphConnector = class extends BaseConnector {
12374
+ constructor() {
12375
+ super(...arguments);
12376
+ this.type = "langgraph";
12377
+ this.name = "LangGraph (REST)";
12378
+ this.supportsStreaming = false;
12379
+ }
12380
+ buildPayload(request2) {
12381
+ const config2 = request2.connectorConfig || {};
12382
+ return {
12383
+ input: {
12384
+ messages: [
12385
+ {
12386
+ role: "user",
12387
+ content: request2.testCase.initialPrompt
12388
+ }
12389
+ ]
12390
+ },
12391
+ config: {
12392
+ configurable: {
12393
+ ...request2.modelId && { model: request2.modelId },
12394
+ ...config2.configurable
12395
+ }
12396
+ }
12397
+ };
12398
+ }
12399
+ async execute(endpoint, request2, auth, onProgress, onRawEvent) {
12400
+ const payload = request2.payload || this.buildPayload(request2);
12401
+ const headers = this.buildAuthHeaders(auth);
12402
+ const config2 = request2.connectorConfig || {};
12403
+ const graphId = config2.graphId || "agent";
12404
+ const baseUrl = endpoint.replace(/\/+$/, "");
12405
+ const threadId = config2.threadId || request2.threadId;
12406
+ const invokeUrl = threadId ? `${baseUrl}/threads/${threadId}/runs/wait` : `${baseUrl}/assistants/${graphId}/invoke`;
12407
+ this.debug("Executing LangGraph request");
12408
+ this.debug("URL:", invokeUrl);
12409
+ const response = await fetch(invokeUrl, {
12410
+ method: "POST",
12411
+ headers: {
12412
+ "Content-Type": "application/json",
12413
+ ...headers
12414
+ },
12415
+ body: JSON.stringify(payload)
12416
+ });
12417
+ if (!response.ok) {
12418
+ const errorText = await response.text();
12419
+ throw new Error(`LangGraph request failed: ${response.status} - ${errorText}`);
12420
+ }
12421
+ const data = await response.json();
12422
+ onRawEvent?.(data);
12423
+ const trajectory = this.parseResponse(data);
12424
+ trajectory.forEach((step) => onProgress?.(step));
12425
+ return {
12426
+ trajectory,
12427
+ runId: data.run_id || data.thread_id || threadId || null,
12428
+ rawEvents: [data],
12429
+ metadata: {
12430
+ graphId,
12431
+ threadId: data.thread_id || threadId
12432
+ }
12433
+ };
12434
+ }
12435
+ parseResponse(data) {
12436
+ const steps = [];
12437
+ const messages = data.output?.messages || data.values?.messages || data.messages || [];
12438
+ for (const msg of messages) {
12439
+ const role = msg.type || msg.role;
12440
+ const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map((c) => c.text || JSON.stringify(c)).join("\n") : JSON.stringify(msg.content);
12441
+ if (role === "ai" || role === "assistant") {
12442
+ if (msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
12443
+ for (const toolCall of msg.tool_calls) {
12444
+ steps.push(this.createStep("action", `Calling ${toolCall.name}...`, {
12445
+ toolName: toolCall.name,
12446
+ toolArgs: toolCall.args
12447
+ }));
12448
+ }
12449
+ }
12450
+ if (content && !msg.tool_calls?.length) {
12451
+ steps.push(this.createStep("assistant", content));
12452
+ }
12453
+ } else if (role === "tool") {
12454
+ steps.push(this.createStep("tool_result", content, {
12455
+ toolName: msg.name,
12456
+ status: "SUCCESS"
12457
+ }));
12458
+ }
12459
+ }
12460
+ const lastAssistant = [...steps].reverse().find((s) => s.type === "assistant");
12461
+ if (lastAssistant) {
12462
+ lastAssistant.type = "response";
12463
+ }
12464
+ if (data.intermediate_steps && Array.isArray(data.intermediate_steps)) {
12465
+ for (const [action, observation] of data.intermediate_steps) {
12466
+ if (action?.tool) {
12467
+ steps.push(this.createStep("action", `Calling ${action.tool}...`, {
12468
+ toolName: action.tool,
12469
+ toolArgs: action.tool_input
12470
+ }));
12471
+ }
12472
+ if (observation !== void 0) {
12473
+ const obsContent = typeof observation === "string" ? observation : JSON.stringify(observation);
12474
+ steps.push(this.createStep("tool_result", obsContent, {
12475
+ status: "SUCCESS"
12476
+ }));
12477
+ }
12478
+ }
12479
+ }
12480
+ if (data.output && typeof data.output === "string" && steps.length === 0) {
12481
+ steps.push(this.createStep("response", data.output));
12482
+ }
12483
+ if (steps.length === 0 && data) {
12484
+ steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
12485
+ }
12486
+ return steps;
12487
+ }
12488
+ async healthCheck(endpoint, auth) {
12489
+ try {
12490
+ const headers = this.buildAuthHeaders(auth);
12491
+ const baseUrl = endpoint.replace(/\/+$/, "");
12492
+ const response = await fetch(`${baseUrl}/ok`, {
12493
+ method: "GET",
12494
+ headers
12495
+ });
12496
+ return response.ok;
12497
+ } catch {
12498
+ try {
12499
+ const headers = this.buildAuthHeaders(auth);
12500
+ const response = await fetch(endpoint, { method: "GET", headers });
12501
+ return response.ok;
12502
+ } catch {
12503
+ return false;
12504
+ }
12505
+ }
12506
+ }
12507
+ };
12508
+ var langgraphConnector = new LangGraphConnector();
12509
+
12177
12510
  // services/connectors/index.ts
12178
12511
  connectorRegistry.register(aguiStreamingConnector);
12179
12512
  connectorRegistry.register(mockConnector);
12180
12513
  connectorRegistry.register(restConnector);
12181
12514
  connectorRegistry.register(openaiCompatibleConnector);
12515
+ connectorRegistry.register(langgraphConnector);
12182
12516
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
12183
12517
 
12184
12518
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -12222,6 +12556,7 @@ Question: ${prompt}`;
12222
12556
  const command = endpoint || this.config.command;
12223
12557
  const args = this.config.args || [];
12224
12558
  const input = request2.payload || this.buildPayload(request2);
12559
+ const runId = `subprocess-${Date.now()}`;
12225
12560
  this.debug("Command:", command);
12226
12561
  this.debug("Args:", args);
12227
12562
  this.debug("Input mode:", this.config.inputMode);
@@ -12229,10 +12564,12 @@ Question: ${prompt}`;
12229
12564
  this.debug("Timeout:", this.config.timeout);
12230
12565
  this.debug("Input (first 500 chars):", input.substring(0, 500));
12231
12566
  this.debug("Working dir:", this.config.workingDir || process.cwd());
12567
+ this.debug("Run ID:", runId);
12232
12568
  const env = {
12233
12569
  ...process.env,
12234
12570
  ...this.buildAuthEnv(auth),
12235
- ...this.config.env
12571
+ ...this.config.env,
12572
+ AGENT_EVAL_RUN_ID: runId
12236
12573
  };
12237
12574
  return new Promise((resolve2, reject) => {
12238
12575
  const trajectory = [];
@@ -12291,6 +12628,17 @@ Question: ${prompt}`;
12291
12628
  this.error(`Process exited with code ${code}`);
12292
12629
  this.error("stderr:", stderr);
12293
12630
  }
12631
+ if (this.config.outputParser === "streaming") {
12632
+ this.onBeforeStreamEnd(trajectory, onProgress);
12633
+ if (code !== 0 && trajectory.length === 0) {
12634
+ const errorContent = stderr.trim() ? `Error: Process exited with code ${code}. ${stderr.trim()}` : `Error: Process exited with code ${code}`;
12635
+ const errorStep = this.createStep("tool_result", errorContent, {
12636
+ status: "FAILURE" /* FAILURE */
12637
+ });
12638
+ trajectory.push(errorStep);
12639
+ onProgress?.(errorStep);
12640
+ }
12641
+ }
12294
12642
  const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code });
12295
12643
  if (this.config.outputParser !== "streaming") {
12296
12644
  finalTrajectory.forEach((step) => onProgress?.(step));
@@ -12298,7 +12646,7 @@ Question: ${prompt}`;
12298
12646
  this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
12299
12647
  resolve2({
12300
12648
  trajectory: finalTrajectory,
12301
- runId: `subprocess-${Date.now()}`,
12649
+ runId,
12302
12650
  rawEvents: rawOutput,
12303
12651
  metadata: {
12304
12652
  command,
@@ -12329,6 +12677,12 @@ Question: ${prompt}`;
12329
12677
  });
12330
12678
  this.debug("========== execute() COMPLETED ==========");
12331
12679
  }
12680
+ /**
12681
+ * Hook called before returning the streaming trajectory on process close.
12682
+ * Subclasses can override to flush internal buffers.
12683
+ */
12684
+ onBeforeStreamEnd(_trajectory, _onProgress) {
12685
+ }
12332
12686
  /**
12333
12687
  * Parse streaming output and emit steps in real-time
12334
12688
  */
@@ -12423,6 +12777,7 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12423
12777
  this.name = "Claude Code CLI";
12424
12778
  this.outputBuffer = "";
12425
12779
  this.thinkingBuffer = "";
12780
+ this.textBuffer = "";
12426
12781
  this.isInThinking = false;
12427
12782
  }
12428
12783
  /**
@@ -12492,11 +12847,17 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12492
12847
  if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
12493
12848
  this.thinkingBuffer += event.delta.thinking;
12494
12849
  } else if (event.delta?.type === "text_delta" && event.delta.text) {
12495
- steps.push(this.createStep("assistant", event.delta.text));
12850
+ this.textBuffer += event.delta.text;
12851
+ }
12852
+ } else if (event.type === "content_block_stop") {
12853
+ if (this.thinkingBuffer) {
12854
+ steps.push(this.createStep("thinking", this.thinkingBuffer));
12855
+ this.thinkingBuffer = "";
12856
+ }
12857
+ if (this.textBuffer) {
12858
+ steps.push(this.createStep("assistant", this.textBuffer));
12859
+ this.textBuffer = "";
12496
12860
  }
12497
- } else if (event.type === "content_block_stop" && this.thinkingBuffer) {
12498
- steps.push(this.createStep("thinking", this.thinkingBuffer));
12499
- this.thinkingBuffer = "";
12500
12861
  } else if (event.type === "result" && event.result) {
12501
12862
  steps.push(this.createStep(
12502
12863
  "response",
@@ -12542,8 +12903,41 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12542
12903
  resetState() {
12543
12904
  this.outputBuffer = "";
12544
12905
  this.thinkingBuffer = "";
12906
+ this.textBuffer = "";
12545
12907
  this.isInThinking = false;
12546
12908
  }
12909
+ /**
12910
+ * Flush remaining buffers when the subprocess stream ends.
12911
+ */
12912
+ onBeforeStreamEnd(trajectory, onProgress) {
12913
+ if (this.outputBuffer.trim()) {
12914
+ try {
12915
+ const event = JSON.parse(this.outputBuffer.trim());
12916
+ const steps = this.parseJsonEvent(event);
12917
+ for (const step of steps) {
12918
+ trajectory.push(step);
12919
+ onProgress?.(step);
12920
+ }
12921
+ } catch {
12922
+ const step = this.createStep("assistant", this.outputBuffer.trim());
12923
+ trajectory.push(step);
12924
+ onProgress?.(step);
12925
+ }
12926
+ this.outputBuffer = "";
12927
+ }
12928
+ if (this.thinkingBuffer) {
12929
+ const step = this.createStep("thinking", this.thinkingBuffer);
12930
+ trajectory.push(step);
12931
+ onProgress?.(step);
12932
+ this.thinkingBuffer = "";
12933
+ }
12934
+ if (this.textBuffer) {
12935
+ const step = this.createStep("response", this.textBuffer);
12936
+ trajectory.push(step);
12937
+ onProgress?.(step);
12938
+ this.textBuffer = "";
12939
+ }
12940
+ }
12547
12941
  /**
12548
12942
  * Build CLI args from ClaudeCodeConnectorConfig
12549
12943
  */
@@ -12644,9 +13038,214 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12644
13038
  };
12645
13039
  var claudeCodeConnector = new ClaudeCodeConnector();
12646
13040
 
13041
+ // services/connectors/strands/StrandsConnector.ts
13042
+ var StrandsConnector = class extends BaseConnector {
13043
+ constructor() {
13044
+ super(...arguments);
13045
+ this.type = "strands";
13046
+ this.name = "Amazon Strands";
13047
+ this.supportsStreaming = true;
13048
+ }
13049
+ buildPayload(request2) {
13050
+ const config2 = request2.connectorConfig || {};
13051
+ return {
13052
+ agentId: "",
13053
+ // Set from endpoint in execute()
13054
+ agentAliasId: config2.agentAliasId || "TSTALIASID",
13055
+ sessionId: config2.sessionId || `eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
13056
+ inputText: request2.testCase.initialPrompt,
13057
+ enableTrace: config2.enableTrace !== false
13058
+ };
13059
+ }
13060
+ async execute(endpoint, request2, auth, onProgress, onRawEvent) {
13061
+ const payload = request2.payload || this.buildPayload(request2);
13062
+ payload.agentId = endpoint;
13063
+ const config2 = request2.connectorConfig || {};
13064
+ const region = config2.region || auth.awsRegion || process.env.AWS_REGION || "us-east-1";
13065
+ this.debug("Executing Strands agent");
13066
+ this.debug("Agent ID:", endpoint);
13067
+ this.debug("Alias:", payload.agentAliasId);
13068
+ this.debug("Region:", region);
13069
+ const { BedrockAgentRuntimeClient, InvokeAgentCommand } = await import("@aws-sdk/client-bedrock-agent-runtime");
13070
+ const clientConfig = { region };
13071
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
13072
+ clientConfig.credentials = {
13073
+ accessKeyId: auth.awsAccessKeyId,
13074
+ secretAccessKey: auth.awsSecretAccessKey,
13075
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
13076
+ };
13077
+ }
13078
+ const client = new BedrockAgentRuntimeClient(clientConfig);
13079
+ const command = new InvokeAgentCommand({
13080
+ agentId: payload.agentId,
13081
+ agentAliasId: payload.agentAliasId,
13082
+ sessionId: payload.sessionId,
13083
+ inputText: payload.inputText,
13084
+ enableTrace: payload.enableTrace
13085
+ });
13086
+ const response = await client.send(command);
13087
+ const trajectory = [];
13088
+ let finalOutput = "";
13089
+ if (response.completion) {
13090
+ for await (const event of response.completion) {
13091
+ onRawEvent?.(event);
13092
+ if (event.chunk?.bytes) {
13093
+ const text = new TextDecoder().decode(event.chunk.bytes);
13094
+ finalOutput += text;
13095
+ }
13096
+ if (event.trace?.trace) {
13097
+ const traceSteps = this.parseTraceEvent(event.trace.trace);
13098
+ for (const step of traceSteps) {
13099
+ trajectory.push(step);
13100
+ onProgress?.(step);
13101
+ }
13102
+ }
13103
+ }
13104
+ }
13105
+ const hasResponseFromTrace = trajectory.some((s) => s.type === "response");
13106
+ if (finalOutput && !hasResponseFromTrace) {
13107
+ const responseStep = this.createStep("response", finalOutput);
13108
+ trajectory.push(responseStep);
13109
+ onProgress?.(responseStep);
13110
+ }
13111
+ return {
13112
+ trajectory,
13113
+ runId: payload.sessionId,
13114
+ metadata: {
13115
+ agentId: endpoint,
13116
+ agentAliasId: payload.agentAliasId,
13117
+ sessionId: payload.sessionId,
13118
+ region
13119
+ }
13120
+ };
13121
+ }
13122
+ parseResponse(rawResponse) {
13123
+ if (rawResponse?.trace?.trace) {
13124
+ return this.parseTraceEvent(rawResponse.trace.trace);
13125
+ }
13126
+ if (typeof rawResponse === "string") {
13127
+ return [this.createStep("response", rawResponse)];
13128
+ }
13129
+ return [];
13130
+ }
13131
+ /**
13132
+ * Parse a Bedrock Agent trace event into TrajectorySteps
13133
+ */
13134
+ parseTraceEvent(trace3) {
13135
+ const steps = [];
13136
+ if (trace3.preProcessingTrace) {
13137
+ const pre = trace3.preProcessingTrace;
13138
+ if (pre.modelInvocationOutput?.parsedResponse?.isValid !== void 0) {
13139
+ steps.push(this.createStep(
13140
+ "thinking",
13141
+ `Pre-processing: Input ${pre.modelInvocationOutput.parsedResponse.isValid ? "valid" : "invalid"}` + (pre.modelInvocationOutput.parsedResponse.rationale ? ` \u2014 ${pre.modelInvocationOutput.parsedResponse.rationale}` : "")
13142
+ ));
13143
+ }
13144
+ }
13145
+ if (trace3.orchestrationTrace) {
13146
+ const orch = trace3.orchestrationTrace;
13147
+ if (orch.rationale?.text) {
13148
+ steps.push(this.createStep("thinking", orch.rationale.text));
13149
+ }
13150
+ if (orch.modelInvocationInput?.text) {
13151
+ steps.push(this.createStep("thinking", `Model input: ${this.truncate(orch.modelInvocationInput.text, 500)}`));
13152
+ }
13153
+ if (orch.invocationInput) {
13154
+ const inv = orch.invocationInput;
13155
+ if (inv.actionGroupInvocationInput) {
13156
+ const action = inv.actionGroupInvocationInput;
13157
+ steps.push(this.createStep("action", `Calling ${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`, {
13158
+ toolName: `${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`,
13159
+ toolArgs: action.parameters ? Object.fromEntries(
13160
+ action.parameters.map((p) => [p.name, p.value])
13161
+ ) : void 0
13162
+ }));
13163
+ }
13164
+ if (inv.knowledgeBaseLookupInput) {
13165
+ steps.push(this.createStep("action", `Knowledge base lookup: ${inv.knowledgeBaseLookupInput.text}`, {
13166
+ toolName: "knowledge_base_lookup",
13167
+ toolArgs: { query: inv.knowledgeBaseLookupInput.text }
13168
+ }));
13169
+ }
13170
+ }
13171
+ if (orch.observation) {
13172
+ const obs = orch.observation;
13173
+ if (obs.actionGroupInvocationOutput?.text) {
13174
+ steps.push(this.createStep("tool_result", obs.actionGroupInvocationOutput.text, {
13175
+ status: "SUCCESS"
13176
+ }));
13177
+ }
13178
+ if (obs.knowledgeBaseLookupOutput?.retrievedReferences) {
13179
+ const refs = obs.knowledgeBaseLookupOutput.retrievedReferences;
13180
+ steps.push(this.createStep(
13181
+ "tool_result",
13182
+ `Retrieved ${refs.length} reference(s) from knowledge base`,
13183
+ {
13184
+ status: "SUCCESS",
13185
+ toolOutput: refs.map((r) => ({
13186
+ content: this.truncate(r.content?.text, 200),
13187
+ location: r.location
13188
+ }))
13189
+ }
13190
+ ));
13191
+ }
13192
+ if (obs.finalResponse?.text) {
13193
+ steps.push(this.createStep("response", obs.finalResponse.text));
13194
+ }
13195
+ }
13196
+ }
13197
+ if (trace3.postProcessingTrace) {
13198
+ const post = trace3.postProcessingTrace;
13199
+ if (post.modelInvocationOutput?.parsedResponse?.text) {
13200
+ steps.push(this.createStep(
13201
+ "thinking",
13202
+ `Post-processing: ${this.truncate(post.modelInvocationOutput.parsedResponse.text, 300)}`
13203
+ ));
13204
+ }
13205
+ }
13206
+ if (trace3.failureTrace) {
13207
+ steps.push(this.createStep(
13208
+ "response",
13209
+ `Agent error: ${trace3.failureTrace.failureReason || "Unknown failure"}`
13210
+ ));
13211
+ }
13212
+ return steps;
13213
+ }
13214
+ /**
13215
+ * Health check: verify the agent exists via GetAgent
13216
+ */
13217
+ async healthCheck(endpoint, auth) {
13218
+ try {
13219
+ const config2 = auth;
13220
+ const region = config2.awsRegion || process.env.AWS_REGION || "us-east-1";
13221
+ const { BedrockAgentClient, GetAgentCommand } = await import("@aws-sdk/client-bedrock-agent");
13222
+ const clientConfig = { region };
13223
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
13224
+ clientConfig.credentials = {
13225
+ accessKeyId: auth.awsAccessKeyId,
13226
+ secretAccessKey: auth.awsSecretAccessKey,
13227
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
13228
+ };
13229
+ }
13230
+ const client = new BedrockAgentClient(clientConfig);
13231
+ const result = await client.send(new GetAgentCommand({ agentId: endpoint }));
13232
+ return result.agent?.agentStatus === "PREPARED";
13233
+ } catch (error) {
13234
+ this.error("Health check failed:", error);
13235
+ return false;
13236
+ }
13237
+ }
13238
+ truncate(text, maxLen) {
13239
+ if (!text) return "";
13240
+ return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
13241
+ }
13242
+ };
13243
+ var strandsConnector = new StrandsConnector();
13244
+
12647
13245
  // services/connectors/server.ts
12648
13246
  connectorRegistry.register(subprocessConnector);
12649
13247
  connectorRegistry.register(claudeCodeConnector);
13248
+ connectorRegistry.register(strandsConnector);
12650
13249
  console.log("[Connectors] Server connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
12651
13250
 
12652
13251
  // services/benchmarkRunner.ts
@@ -12752,6 +13351,9 @@ var runStorage = {
12752
13351
  const params = new URLSearchParams();
12753
13352
  if (options.size) params.append("size", options.size.toString());
12754
13353
  if (options.from) params.append("from", options.from.toString());
13354
+ if (options._source && options._source.length > 0) {
13355
+ params.append("fields", options._source.join(","));
13356
+ }
12755
13357
  const query = params.toString() ? `?${params.toString()}` : "";
12756
13358
  return request("GET", `/runs${query}`);
12757
13359
  },
@@ -13398,7 +14000,489 @@ var TracePollingManager = class {
13398
14000
  };
13399
14001
  var tracePollingManager = new TracePollingManager();
13400
14002
 
14003
+ // lib/telemetry/provider.ts
14004
+ import { trace } from "@opentelemetry/api";
14005
+ import { NodeTracerProvider, BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
14006
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
14007
+ import { resourceFromAttributes } from "@opentelemetry/resources";
14008
+
14009
+ // lib/telemetry/constants.ts
14010
+ import {
14011
+ ATTR_TEST_SUITE_NAME,
14012
+ ATTR_TEST_SUITE_RUN_STATUS,
14013
+ ATTR_TEST_CASE_NAME,
14014
+ ATTR_TEST_CASE_RESULT_STATUS,
14015
+ TEST_SUITE_RUN_STATUS_VALUE_SUCCESS,
14016
+ TEST_SUITE_RUN_STATUS_VALUE_FAILURE,
14017
+ TEST_SUITE_RUN_STATUS_VALUE_IN_PROGRESS,
14018
+ TEST_CASE_RESULT_STATUS_VALUE_PASS,
14019
+ TEST_CASE_RESULT_STATUS_VALUE_FAIL,
14020
+ ATTR_GEN_AI_OPERATION_NAME as ATTR_GEN_AI_OPERATION_NAME4,
14021
+ ATTR_GEN_AI_EVALUATION_NAME,
14022
+ ATTR_GEN_AI_EVALUATION_SCORE_VALUE,
14023
+ ATTR_GEN_AI_EVALUATION_SCORE_LABEL,
14024
+ ATTR_GEN_AI_EVALUATION_EXPLANATION,
14025
+ EVENT_GEN_AI_EVALUATION_RESULT
14026
+ } from "@opentelemetry/semantic-conventions/incubating";
14027
+ var ATTR_TEST_SUITE_RUN_ID = "test.suite.run.id";
14028
+ var ATTR_TEST_CASE_ID = "test.case.id";
14029
+ var ATTR_TEST_CASE_INPUT = "test.case.input";
14030
+ var ATTR_TEST_CASE_OUTPUT = "test.case.output";
14031
+ var ATTR_TEST_CASE_EXPECTED = "test.case.expected";
14032
+ var ATTR_AGENT_HEALTH_JUDGE_MODEL_ID = "agent_health.judge.model_id";
14033
+ var ATTR_AGENT_HEALTH_JUDGE_DURATION_MS = "agent_health.judge.duration_ms";
14034
+ var ATTR_AGENT_HEALTH_JUDGE_ATTEMPTS = "agent_health.judge.attempts";
14035
+ var ATTR_AGENT_HEALTH_AGENT_DURATION_MS = "agent_health.agent.duration_ms";
14036
+ var ATTR_AGENT_HEALTH_CONNECTOR_PROTOCOL = "agent_health.connector.protocol";
14037
+ var ATTR_AGENT_HEALTH_AGENT_RUN_ID = "gen_ai.request.id";
14038
+ var GEN_AI_OPERATION_NAME_VALUE_EVALUATION = "evaluation";
14039
+ var MAX_ATTRIBUTE_LENGTH = 1e4;
14040
+ var MAX_EXPLANATION_LENGTH = 4e3;
14041
+ var EVAL_TRACER_NAME = "agent-health-eval";
14042
+
14043
+ // lib/telemetry/opensearchExporter.ts
14044
+ import { ExportResultCode } from "@opentelemetry/core";
14045
+ import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
14046
+ import { Client as Client2 } from "@opensearch-project/opensearch";
14047
+ function hrTimeToDate(hrTime) {
14048
+ const ms = hrTime[0] * 1e3 + hrTime[1] / 1e6;
14049
+ return new Date(ms).toISOString();
14050
+ }
14051
+ function hrTimeToNanos(hrTime) {
14052
+ return hrTime[0] * 1e9 + hrTime[1];
14053
+ }
14054
+ function durationNanos(start, end) {
14055
+ return hrTimeToNanos(end) - hrTimeToNanos(start);
14056
+ }
14057
+ function flattenKey(key) {
14058
+ return key.replace(/\./g, "@");
14059
+ }
14060
+ function spanKindToString(kind) {
14061
+ switch (kind) {
14062
+ case SpanKind.INTERNAL:
14063
+ return "SPAN_KIND_INTERNAL";
14064
+ case SpanKind.SERVER:
14065
+ return "SPAN_KIND_SERVER";
14066
+ case SpanKind.CLIENT:
14067
+ return "SPAN_KIND_CLIENT";
14068
+ case SpanKind.PRODUCER:
14069
+ return "SPAN_KIND_PRODUCER";
14070
+ case SpanKind.CONSUMER:
14071
+ return "SPAN_KIND_CONSUMER";
14072
+ default:
14073
+ return "SPAN_KIND_INTERNAL";
14074
+ }
14075
+ }
14076
+ function statusCodeToInt(code) {
14077
+ switch (code) {
14078
+ case SpanStatusCode.UNSET:
14079
+ return 0;
14080
+ case SpanStatusCode.OK:
14081
+ return 1;
14082
+ case SpanStatusCode.ERROR:
14083
+ return 2;
14084
+ default:
14085
+ return 0;
14086
+ }
14087
+ }
14088
+ function spanToDocument(span) {
14089
+ const doc = {
14090
+ traceId: span.spanContext().traceId,
14091
+ spanId: span.spanContext().spanId,
14092
+ parentSpanId: span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== "0000000000000000" ? span.parentSpanContext.spanId : "",
14093
+ name: span.name,
14094
+ kind: spanKindToString(span.kind),
14095
+ startTime: hrTimeToDate(span.startTime),
14096
+ endTime: hrTimeToDate(span.endTime),
14097
+ durationInNanos: durationNanos(span.startTime, span.endTime),
14098
+ "status.code": statusCodeToInt(span.status.code),
14099
+ "status.message": span.status.message || "",
14100
+ traceState: "",
14101
+ droppedAttributesCount: span.droppedAttributesCount,
14102
+ droppedEventsCount: span.droppedEventsCount,
14103
+ droppedLinksCount: span.droppedLinksCount
14104
+ };
14105
+ for (const [key, value] of Object.entries(span.attributes)) {
14106
+ if (value !== void 0) {
14107
+ doc[`span.attributes.${flattenKey(key)}`] = value;
14108
+ }
14109
+ }
14110
+ const resource = span.resource;
14111
+ if (resource?.attributes) {
14112
+ for (const [key, value] of Object.entries(resource.attributes)) {
14113
+ if (value !== void 0) {
14114
+ doc[`resource.attributes.${flattenKey(key)}`] = value;
14115
+ }
14116
+ }
14117
+ }
14118
+ const serviceName = resource?.attributes?.["service.name"] || "unknown";
14119
+ doc.serviceName = serviceName;
14120
+ const scope = span.instrumentationScope ?? span.instrumentationLibrary ?? {};
14121
+ doc["instrumentationScope.name"] = scope.name || "";
14122
+ if (scope.version) {
14123
+ doc["instrumentationScope.version"] = scope.version;
14124
+ }
14125
+ if (span.events.length > 0) {
14126
+ doc.events = span.events.map((event) => {
14127
+ const eventDoc = {
14128
+ name: event.name,
14129
+ time: hrTimeToDate(event.time),
14130
+ droppedAttributesCount: event.droppedAttributesCount || 0
14131
+ };
14132
+ if (event.attributes) {
14133
+ const attrs = {};
14134
+ for (const [key, value] of Object.entries(event.attributes)) {
14135
+ if (value !== void 0) {
14136
+ attrs[flattenKey(key)] = value;
14137
+ }
14138
+ }
14139
+ eventDoc.attributes = attrs;
14140
+ }
14141
+ return eventDoc;
14142
+ });
14143
+ } else {
14144
+ doc.events = [];
14145
+ }
14146
+ if (span.links.length > 0) {
14147
+ doc.links = span.links.map((link) => ({
14148
+ traceId: link.context.traceId,
14149
+ spanId: link.context.spanId,
14150
+ traceState: link.context.traceState?.serialize() || "",
14151
+ attributes: link.attributes || {},
14152
+ droppedAttributesCount: link.droppedAttributesCount || 0
14153
+ }));
14154
+ } else {
14155
+ doc.links = [];
14156
+ }
14157
+ const hasRealParent = span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== "0000000000000000";
14158
+ if (!hasRealParent) {
14159
+ doc.traceGroup = span.name;
14160
+ doc.traceGroupFields = {
14161
+ endTime: hrTimeToDate(span.endTime),
14162
+ durationInNanos: durationNanos(span.startTime, span.endTime),
14163
+ statusCode: statusCodeToInt(span.status.code)
14164
+ };
14165
+ }
14166
+ return doc;
14167
+ }
14168
+ var OpenSearchSpanExporter = class {
14169
+ constructor(config2) {
14170
+ this.shutdownRequested = false;
14171
+ const auth = config2.username && config2.password ? `${config2.username}:${config2.password}` : void 0;
14172
+ this.client = new Client2({
14173
+ node: config2.endpoint,
14174
+ auth: auth ? { username: config2.username, password: config2.password } : void 0,
14175
+ ssl: {
14176
+ rejectUnauthorized: config2.tlsSkipVerify !== true
14177
+ }
14178
+ });
14179
+ this.indexName = config2.indexName || "otel-v1-apm-span-000001";
14180
+ console.log(`[Telemetry] OpenSearch exporter initialized \u2192 ${config2.endpoint} (index: ${this.indexName})`);
14181
+ }
14182
+ export(spans, resultCallback) {
14183
+ if (this.shutdownRequested) {
14184
+ resultCallback({ code: ExportResultCode.FAILED });
14185
+ return;
14186
+ }
14187
+ this._export(spans).then(() => resultCallback({ code: ExportResultCode.SUCCESS })).catch((err) => {
14188
+ console.error("[Telemetry] OpenSearch export failed:", err instanceof Error ? err.message : err);
14189
+ resultCallback({ code: ExportResultCode.FAILED });
14190
+ });
14191
+ }
14192
+ async _export(spans) {
14193
+ if (spans.length === 0) return;
14194
+ const body = [];
14195
+ for (const span of spans) {
14196
+ const doc = spanToDocument(span);
14197
+ const docId = `${doc.traceId}/${doc.spanId}`;
14198
+ body.push({ index: { _index: this.indexName, _id: docId } });
14199
+ body.push(doc);
14200
+ }
14201
+ const response = await this.client.bulk({ body });
14202
+ if (response.body.errors) {
14203
+ const errors = response.body.items.filter((item) => item.index?.error).map((item) => `${item.index.error.type}: ${item.index.error.reason}`);
14204
+ if (errors.length > 0) {
14205
+ console.warn(`[Telemetry] OpenSearch bulk write had ${errors.length} error(s):`, errors[0]);
14206
+ }
14207
+ } else {
14208
+ console.log(`[Telemetry] Exported ${spans.length} span(s) to OpenSearch`);
14209
+ }
14210
+ }
14211
+ async shutdown() {
14212
+ this.shutdownRequested = true;
14213
+ await this.client.close();
14214
+ }
14215
+ async forceFlush() {
14216
+ }
14217
+ };
14218
+
14219
+ // lib/telemetry/provider.ts
14220
+ var provider = null;
14221
+ var telemetryEnabled = false;
14222
+ function resolveEvalTelemetryConfig(userConfig) {
14223
+ const enabled = userConfig?.enabled ?? process.env.OTEL_EVAL_ENABLED === "true";
14224
+ const exporterEndpoint = userConfig?.exporterEndpoint ?? process.env.OTEL_EVAL_EXPORTER_ENDPOINT ?? "http://localhost:4318/v1/traces";
14225
+ let exporterHeaders = userConfig?.exporterHeaders;
14226
+ if (!exporterHeaders && process.env.OTEL_EVAL_EXPORTER_HEADERS) {
14227
+ try {
14228
+ exporterHeaders = JSON.parse(process.env.OTEL_EVAL_EXPORTER_HEADERS);
14229
+ } catch {
14230
+ console.warn("[Telemetry] Failed to parse OTEL_EVAL_EXPORTER_HEADERS as JSON");
14231
+ }
14232
+ }
14233
+ const serviceName = userConfig?.serviceName ?? process.env.OTEL_EVAL_SERVICE_NAME ?? "agent-health";
14234
+ return { enabled, exporterEndpoint, exporterHeaders, serviceName, opensearch: userConfig?.opensearch };
14235
+ }
14236
+ function initEvalTracerProvider(config2) {
14237
+ if (provider) {
14238
+ return;
14239
+ }
14240
+ if (!config2.enabled) {
14241
+ telemetryEnabled = false;
14242
+ console.log("[Telemetry] Evaluation telemetry disabled (OTEL_EVAL_ENABLED != true)");
14243
+ return;
14244
+ }
14245
+ const resource = resourceFromAttributes({
14246
+ "service.name": config2.serviceName ?? "agent-health",
14247
+ "telemetry.sdk.name": "agent-health",
14248
+ "telemetry.sdk.language": "nodejs"
14249
+ });
14250
+ const spanProcessors = [];
14251
+ const hasExplicitOtlpEndpoint = config2.exporterEndpoint !== "http://localhost:4318/v1/traces";
14252
+ if (hasExplicitOtlpEndpoint) {
14253
+ const otlpExporter = new OTLPTraceExporter({
14254
+ url: config2.exporterEndpoint,
14255
+ headers: config2.exporterHeaders
14256
+ });
14257
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter));
14258
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OTLP (${config2.exporterEndpoint})`);
14259
+ } else if (config2.opensearch) {
14260
+ const osExporter = new OpenSearchSpanExporter(config2.opensearch);
14261
+ spanProcessors.push(new SimpleSpanProcessor(osExporter));
14262
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OpenSearch direct (${config2.opensearch.endpoint})`);
14263
+ } else {
14264
+ const otlpExporter = new OTLPTraceExporter({
14265
+ url: config2.exporterEndpoint,
14266
+ headers: config2.exporterHeaders
14267
+ });
14268
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter));
14269
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OTLP (${config2.exporterEndpoint})`);
14270
+ }
14271
+ provider = new NodeTracerProvider({
14272
+ resource,
14273
+ spanProcessors
14274
+ });
14275
+ provider.register();
14276
+ telemetryEnabled = true;
14277
+ }
14278
+ function getEvalTracer() {
14279
+ return trace.getTracer(EVAL_TRACER_NAME);
14280
+ }
14281
+ function isEvalTelemetryEnabled() {
14282
+ return telemetryEnabled;
14283
+ }
14284
+ async function flushEvalTracer() {
14285
+ if (provider) {
14286
+ try {
14287
+ await provider.forceFlush();
14288
+ } catch (err) {
14289
+ console.warn("[Telemetry] forceFlush failed:", err instanceof Error ? err.message : err);
14290
+ }
14291
+ }
14292
+ }
14293
+ async function shutdownEvalTracer() {
14294
+ if (provider) {
14295
+ await provider.shutdown();
14296
+ provider = null;
14297
+ telemetryEnabled = false;
14298
+ console.log("[Telemetry] Evaluation telemetry shut down");
14299
+ }
14300
+ }
14301
+
14302
+ // lib/telemetry/evalSpans.ts
14303
+ import {
14304
+ context,
14305
+ trace as trace2,
14306
+ TraceFlags,
14307
+ SpanKind as SpanKind2,
14308
+ SpanStatusCode as SpanStatusCode2
14309
+ } from "@opentelemetry/api";
14310
+ function truncate(value, maxLength) {
14311
+ if (!value) return void 0;
14312
+ return value.length > maxLength ? value.slice(0, maxLength) + "..." : value;
14313
+ }
14314
+ function extractAgentResponse(report) {
14315
+ const responseStep = [...report.trajectory].reverse().find(
14316
+ (s) => s.type === "response" || s.type === "assistant"
14317
+ );
14318
+ return responseStep?.content;
14319
+ }
14320
+ function startTestSuiteRunSpan(benchmark, run) {
14321
+ if (!isEvalTelemetryEnabled()) return null;
14322
+ const tracer = getEvalTracer();
14323
+ const span = tracer.startSpan(`test_suite_run ${benchmark.name}`, {
14324
+ kind: SpanKind2.INTERNAL,
14325
+ attributes: {
14326
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14327
+ [ATTR_TEST_SUITE_RUN_ID]: run.id,
14328
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14329
+ }
14330
+ });
14331
+ const ctx = context.active();
14332
+ const spanContext = trace2.setSpan(ctx, span);
14333
+ console.log(`[Telemetry] Started test_suite_run span for "${benchmark.name}" (runId=${run.id})`);
14334
+ return { span, context: spanContext };
14335
+ }
14336
+ function startTestCaseSpan(parentCtx, testCase, benchmark, run, agentRunId, links) {
14337
+ if (!isEvalTelemetryEnabled()) return null;
14338
+ const tracer = getEvalTracer();
14339
+ const attributes = {
14340
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14341
+ [ATTR_TEST_SUITE_RUN_ID]: run.id,
14342
+ [ATTR_TEST_CASE_ID]: testCase.id,
14343
+ [ATTR_TEST_CASE_NAME]: testCase.name,
14344
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14345
+ };
14346
+ if (agentRunId) {
14347
+ attributes[ATTR_AGENT_HEALTH_AGENT_RUN_ID] = agentRunId;
14348
+ }
14349
+ attributes[ATTR_TEST_CASE_INPUT] = truncate(testCase.initialPrompt || "", MAX_ATTRIBUTE_LENGTH);
14350
+ const expected = truncate(
14351
+ testCase.expectedOutcomes ? JSON.stringify(testCase.expectedOutcomes) : void 0,
14352
+ MAX_ATTRIBUTE_LENGTH
14353
+ );
14354
+ if (expected) attributes[ATTR_TEST_CASE_EXPECTED] = expected;
14355
+ const span = tracer.startSpan(
14356
+ "test_case",
14357
+ {
14358
+ kind: SpanKind2.INTERNAL,
14359
+ attributes,
14360
+ links
14361
+ },
14362
+ parentCtx
14363
+ );
14364
+ const ctx = trace2.setSpan(parentCtx, span);
14365
+ console.log(`[Telemetry] Started test_case span for "${testCase.name}" (agentRunId=${agentRunId || "none"})`);
14366
+ return { span, context: ctx };
14367
+ }
14368
+ function addEvaluationResultEvents(span, report) {
14369
+ if (!isEvalTelemetryEnabled()) return;
14370
+ if (report.metrics?.accuracy !== void 0) {
14371
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14372
+ [ATTR_GEN_AI_EVALUATION_NAME]: "accuracy",
14373
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.accuracy,
14374
+ [ATTR_GEN_AI_EVALUATION_SCORE_LABEL]: report.passFailStatus === "passed" ? "pass" : "fail",
14375
+ [ATTR_GEN_AI_EVALUATION_EXPLANATION]: truncate(report.llmJudgeReasoning, MAX_EXPLANATION_LENGTH)
14376
+ });
14377
+ }
14378
+ if (report.metrics?.faithfulness !== void 0) {
14379
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14380
+ [ATTR_GEN_AI_EVALUATION_NAME]: "faithfulness",
14381
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.faithfulness
14382
+ });
14383
+ }
14384
+ if (report.metrics?.latency_score !== void 0) {
14385
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14386
+ [ATTR_GEN_AI_EVALUATION_NAME]: "latency_score",
14387
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.latency_score
14388
+ });
14389
+ }
14390
+ if (report.metrics?.trajectory_alignment_score !== void 0) {
14391
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14392
+ [ATTR_GEN_AI_EVALUATION_NAME]: "trajectory_alignment_score",
14393
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.trajectory_alignment_score
14394
+ });
14395
+ }
14396
+ }
14397
+ function finalizeTestCaseSpan(span, report, endTime) {
14398
+ if (!isEvalTelemetryEnabled()) return;
14399
+ const status = report.passFailStatus === "passed" ? TEST_CASE_RESULT_STATUS_VALUE_PASS : TEST_CASE_RESULT_STATUS_VALUE_FAIL;
14400
+ span.setAttribute(ATTR_TEST_CASE_RESULT_STATUS, status);
14401
+ const output = extractAgentResponse(report) || "";
14402
+ span.setAttribute(ATTR_TEST_CASE_OUTPUT, truncate(output, MAX_ATTRIBUTE_LENGTH));
14403
+ if (report.connectorProtocol) {
14404
+ span.setAttribute(ATTR_AGENT_HEALTH_CONNECTOR_PROTOCOL, report.connectorProtocol);
14405
+ }
14406
+ if (report.performanceMetrics?.agentDurationMs !== void 0) {
14407
+ span.setAttribute(ATTR_AGENT_HEALTH_AGENT_DURATION_MS, report.performanceMetrics.agentDurationMs);
14408
+ }
14409
+ if (report.performanceMetrics?.judgeDurationMs !== void 0) {
14410
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_DURATION_MS, report.performanceMetrics.judgeDurationMs);
14411
+ }
14412
+ if (report.performanceMetrics?.judgeAttempts !== void 0) {
14413
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_ATTEMPTS, report.performanceMetrics.judgeAttempts);
14414
+ }
14415
+ if (report.llmJudgeResponse?.modelId) {
14416
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_MODEL_ID, report.llmJudgeResponse.modelId);
14417
+ }
14418
+ if (report.status === "failed") {
14419
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: "Evaluation failed" });
14420
+ }
14421
+ span.end(endTime);
14422
+ console.log(`[Telemetry] Finalized test_case span (status=${status}, passFailStatus=${report.passFailStatus})`);
14423
+ }
14424
+ function finalizeTestSuiteRunSpan(span, run) {
14425
+ if (!isEvalTelemetryEnabled()) return;
14426
+ const results = Object.values(run.results);
14427
+ const hasFailures = results.some((r) => r.status === "failed");
14428
+ const status = hasFailures ? TEST_SUITE_RUN_STATUS_VALUE_FAILURE : TEST_SUITE_RUN_STATUS_VALUE_SUCCESS;
14429
+ span.setAttribute(ATTR_TEST_SUITE_RUN_STATUS, status);
14430
+ if (hasFailures) {
14431
+ const failCount = results.filter((r) => r.status === "failed").length;
14432
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: `${failCount} case(s) failed` });
14433
+ }
14434
+ span.end();
14435
+ console.log(`[Telemetry] Finalized test_suite_run span (status=${status}), flushing...`);
14436
+ flushEvalTracer().catch(() => {
14437
+ });
14438
+ }
14439
+ function emitDeferredTestCaseSpan(testCase, report, benchmark, runId, agentRunId, startTime, endTime, agentTraceId) {
14440
+ if (!isEvalTelemetryEnabled()) return;
14441
+ const tracer = getEvalTracer();
14442
+ const attributes = {
14443
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14444
+ [ATTR_TEST_SUITE_RUN_ID]: runId,
14445
+ [ATTR_TEST_CASE_ID]: testCase.id,
14446
+ [ATTR_TEST_CASE_NAME]: testCase.name,
14447
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14448
+ };
14449
+ if (agentRunId) {
14450
+ attributes[ATTR_AGENT_HEALTH_AGENT_RUN_ID] = agentRunId;
14451
+ }
14452
+ attributes[ATTR_TEST_CASE_INPUT] = truncate(testCase.initialPrompt || "", MAX_ATTRIBUTE_LENGTH);
14453
+ const expected = truncate(
14454
+ testCase.expectedOutcomes ? JSON.stringify(testCase.expectedOutcomes) : void 0,
14455
+ MAX_ATTRIBUTE_LENGTH
14456
+ );
14457
+ if (expected) attributes[ATTR_TEST_CASE_EXPECTED] = expected;
14458
+ let parentCtx;
14459
+ if (agentTraceId) {
14460
+ const remoteSpanContext = {
14461
+ traceId: agentTraceId,
14462
+ // Use a valid synthetic spanId — an all-zero spanId is considered invalid
14463
+ // by the OTel SDK and would cause it to ignore this context entirely.
14464
+ // This "remote" context carries the traceId forward so the eval span
14465
+ // shares the same trace as the agent spans.
14466
+ spanId: "eeee000000000001",
14467
+ traceFlags: TraceFlags.SAMPLED,
14468
+ isRemote: true
14469
+ };
14470
+ parentCtx = trace2.setSpanContext(context.active(), remoteSpanContext);
14471
+ }
14472
+ const span = tracer.startSpan("test_case", {
14473
+ kind: SpanKind2.INTERNAL,
14474
+ startTime,
14475
+ attributes
14476
+ }, parentCtx);
14477
+ addEvaluationResultEvents(span, report);
14478
+ finalizeTestCaseSpan(span, report, endTime);
14479
+ console.log(`[Telemetry] Emitted deferred test_case span for "${testCase.name}" (agentRunId=${agentRunId || "none"}, traceId=${agentTraceId || "new"}), flushing...`);
14480
+ flushEvalTracer().catch(() => {
14481
+ });
14482
+ }
14483
+
13401
14484
  // services/benchmarkRunner.ts
14485
+ import { SpanStatusCode as SpanStatusCode3 } from "@opentelemetry/api";
13402
14486
  function getConfig() {
13403
14487
  try {
13404
14488
  return loadConfigSync();
@@ -13456,6 +14540,9 @@ async function executeRun(benchmark, run, onProgress, options) {
13456
14540
  const concurrency = run.concurrency ?? 1;
13457
14541
  const runStartTime = Date.now();
13458
14542
  console.log(`[BenchmarkRunner] Starting run ${run.id} with concurrency=${concurrency} for ${totalTestCases} test cases`);
14543
+ const suiteSpanResult = startTestSuiteRunSpan(benchmark, run);
14544
+ const suiteSpan = suiteSpanResult?.span;
14545
+ const suiteContext = suiteSpanResult?.context;
13459
14546
  if (!run.results) {
13460
14547
  run.results = {};
13461
14548
  }
@@ -13506,6 +14593,7 @@ async function executeRun(benchmark, run, onProgress, options) {
13506
14593
  });
13507
14594
  debug("BenchmarkRunner", `[${testCaseId}] Starting evaluation (${completedCount}/${totalTestCases} completed)`);
13508
14595
  const testCaseStartTime = Date.now();
14596
+ let caseSpan;
13509
14597
  run.results[testCaseId] = { reportId: "", status: "running" };
13510
14598
  try {
13511
14599
  const agentConfig = buildAgentConfigForRun(run);
@@ -13525,7 +14613,23 @@ async function executeRun(benchmark, run, onProgress, options) {
13525
14613
  });
13526
14614
  updateTestCaseLastRunAt(client, testCaseId, (/* @__PURE__ */ new Date()).toISOString()).catch((err) => console.warn(`[BenchmarkRunner] Failed to update lastRunAt for ${testCaseId}:`, err.message));
13527
14615
  if (savedReport.metricsStatus === "pending" && savedReport.runId) {
13528
- startTracePollingForReport(savedReport, testCase, client);
14616
+ startTracePollingForReport(savedReport, testCase, client, benchmark, run);
14617
+ }
14618
+ if (suiteContext) {
14619
+ const caseSpanResult = startTestCaseSpan(
14620
+ suiteContext,
14621
+ testCase,
14622
+ benchmark,
14623
+ run,
14624
+ savedReport.runId
14625
+ );
14626
+ caseSpan = caseSpanResult?.span;
14627
+ if (caseSpan && savedReport.metricsStatus !== "pending") {
14628
+ addEvaluationResultEvents(caseSpan, savedReport);
14629
+ finalizeTestCaseSpan(caseSpan, savedReport);
14630
+ } else if (caseSpan) {
14631
+ caseSpan.end();
14632
+ }
13529
14633
  }
13530
14634
  run.results[testCaseId] = {
13531
14635
  reportId: savedReport.id,
@@ -13544,6 +14648,10 @@ async function executeRun(benchmark, run, onProgress, options) {
13544
14648
  const testCaseDuration = Date.now() - testCaseStartTime;
13545
14649
  debug("BenchmarkRunner", `[${testCaseId}] Failed in ${testCaseDuration}ms: ${errorMsg}`);
13546
14650
  run.results[testCaseId] = { reportId: "", status: "failed", error: errorMsg };
14651
+ if (caseSpan) {
14652
+ caseSpan.setStatus({ code: SpanStatusCode3.ERROR, message: errorMsg });
14653
+ caseSpan.end();
14654
+ }
13547
14655
  completedCount++;
13548
14656
  if (errorMsg.includes("ThrottlingException") || errorMsg.includes("rate limit") || errorMsg.includes("429")) {
13549
14657
  consecutiveThrottles++;
@@ -13587,6 +14695,9 @@ async function executeRun(benchmark, run, onProgress, options) {
13587
14695
  maxTestCaseDurationMs: testCaseDurations.length > 0 ? Math.max(...testCaseDurations) : 0,
13588
14696
  minTestCaseDurationMs: testCaseDurations.length > 0 ? Math.min(...testCaseDurations) : 0
13589
14697
  };
14698
+ if (suiteSpan) {
14699
+ finalizeTestSuiteRunSpan(suiteSpan, run);
14700
+ }
13590
14701
  return run;
13591
14702
  } catch (error) {
13592
14703
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -13595,6 +14706,10 @@ async function executeRun(benchmark, run, onProgress, options) {
13595
14706
  run.results[testCaseId] = { reportId: "", status: "failed", error: `Benchmark execution failed: ${errorMsg}` };
13596
14707
  }
13597
14708
  });
14709
+ if (suiteSpan) {
14710
+ suiteSpan.setStatus({ code: SpanStatusCode3.ERROR, message: errorMsg });
14711
+ suiteSpan.end();
14712
+ }
13598
14713
  throw error;
13599
14714
  }
13600
14715
  }
@@ -13626,6 +14741,7 @@ async function saveReportWithModule(storage, report) {
13626
14741
  async function runSingleUseCase(run, testCase, storage, onStep) {
13627
14742
  const agentConfig = buildAgentConfigForRun(run);
13628
14743
  const bedrockModelId = getBedrockModelId(run.modelId);
14744
+ const startTime = /* @__PURE__ */ new Date();
13629
14745
  const report = await runEvaluationWithConnector(
13630
14746
  agentConfig,
13631
14747
  bedrockModelId,
@@ -13643,6 +14759,17 @@ async function runSingleUseCase(run, testCase, storage, onStep) {
13643
14759
  if (savedReport.metricsStatus === "pending" && savedReport.runId) {
13644
14760
  startTracePollingForReportWithModule(savedReport, testCase, storage);
13645
14761
  }
14762
+ if (savedReport.metricsStatus !== "pending") {
14763
+ emitDeferredTestCaseSpan(
14764
+ testCase,
14765
+ savedReport,
14766
+ { name: `standalone:${agentConfig.name || agentConfig.key}` },
14767
+ run.id,
14768
+ savedReport.runId,
14769
+ startTime,
14770
+ /* @__PURE__ */ new Date()
14771
+ );
14772
+ }
13646
14773
  return savedReport.id;
13647
14774
  }
13648
14775
  function startTracePollingForReportWithModule(report, testCase, storage) {
@@ -13682,6 +14809,24 @@ function startTracePollingForReportWithModule(report, testCase, storage) {
13682
14809
  llmJudgeReasoning: judgment.llmJudgeReasoning,
13683
14810
  improvementStrategies: judgment.improvementStrategies
13684
14811
  });
14812
+ const completedReport = {
14813
+ ...report,
14814
+ passFailStatus: judgment.passFailStatus,
14815
+ metrics: judgment.metrics,
14816
+ llmJudgeReasoning: judgment.llmJudgeReasoning
14817
+ };
14818
+ emitDeferredTestCaseSpan(
14819
+ testCase,
14820
+ completedReport,
14821
+ { name: report.experimentId ? `benchmark:${report.experimentId}` : `standalone:${report.agentKey}` },
14822
+ report.experimentRunId || report.id,
14823
+ report.runId,
14824
+ void 0,
14825
+ // startTime
14826
+ void 0,
14827
+ // endTime
14828
+ spans[0]?.traceId
14829
+ );
13685
14830
  if (report.experimentId) {
13686
14831
  await refreshBenchmarkRunStats(storage, report.experimentId, report.id);
13687
14832
  }
@@ -13709,7 +14854,7 @@ function startTracePollingForReportWithModule(report, testCase, storage) {
13709
14854
  }
13710
14855
  );
13711
14856
  }
13712
- function startTracePollingForReport(report, testCase, client) {
14857
+ function startTracePollingForReport(report, testCase, client, benchmark, run) {
13713
14858
  if (!report.runId) {
13714
14859
  console.warn(`[BenchmarkRunner] No runId for report ${report.id}, cannot start trace polling`);
13715
14860
  return;
@@ -13744,6 +14889,16 @@ function startTracePollingForReport(report, testCase, client) {
13744
14889
  if (report.experimentId) {
13745
14890
  await updateBenchmarkRunStatsForReport(client, report.experimentId, report.id);
13746
14891
  }
14892
+ if (benchmark && run) {
14893
+ const completedReport = {
14894
+ ...report,
14895
+ passFailStatus: judgment.passFailStatus,
14896
+ metrics: judgment.metrics,
14897
+ llmJudgeReasoning: judgment.llmJudgeReasoning
14898
+ };
14899
+ const agentTraceId = spans[0]?.traceId;
14900
+ emitDeferredTestCaseSpan(testCase, completedReport, benchmark, run.id, report.runId, void 0, void 0, agentTraceId);
14901
+ }
13747
14902
  } catch (error) {
13748
14903
  console.error(`[BenchmarkRunner] Failed to judge report ${report.id}:`, error instanceof Error ? error.message : error);
13749
14904
  await updateRunWithClient(client, report.id, {
@@ -15179,11 +16334,13 @@ router10.get("/api/storage/runs", async (req, res) => {
15179
16334
  try {
15180
16335
  const { size = "100", from = "0", fields } = req.query;
15181
16336
  let realData = [];
16337
+ const fieldList = typeof fields === "string" ? fields.split(",").map((f) => f.trim()).filter(Boolean) : void 0;
15182
16338
  const storage = getStorageModule();
15183
16339
  try {
15184
16340
  const result = await storage.runs.getAll({
15185
16341
  size: parseInt(size),
15186
- from: parseInt(from)
16342
+ from: parseInt(from),
16343
+ _source: fieldList
15187
16344
  });
15188
16345
  realData = result.items;
15189
16346
  } catch (e) {
@@ -19349,7 +20506,25 @@ async function initializeStorageBackend() {
19349
20506
  }
19350
20507
  async function createApp() {
19351
20508
  await migrateYamlToJsonIfNeeded();
19352
- await loadConfig();
20509
+ const config2 = await loadConfig();
20510
+ const obsConfig = getObservabilityConfigFromFile() ?? getObservabilityConfigFromEnv();
20511
+ let opensearchExporterConfig;
20512
+ if (obsConfig?.endpoint) {
20513
+ opensearchExporterConfig = {
20514
+ endpoint: obsConfig.endpoint,
20515
+ username: obsConfig.username,
20516
+ password: obsConfig.password,
20517
+ tlsSkipVerify: obsConfig.tlsSkipVerify,
20518
+ indexName: "otel-v1-apm-span"
20519
+ };
20520
+ }
20521
+ initEvalTracerProvider(resolveEvalTelemetryConfig({
20522
+ ...config2.telemetry,
20523
+ opensearch: opensearchExporterConfig
20524
+ }));
20525
+ process.once("SIGTERM", async () => {
20526
+ await shutdownEvalTracer();
20527
+ });
19353
20528
  await initializeStorageBackend();
19354
20529
  const app = express2();
19355
20530
  setupMiddleware(app);
@@ -19358,8 +20533,110 @@ async function createApp() {
19358
20533
  return app;
19359
20534
  }
19360
20535
 
20536
+ // server/services/observioAgent.ts
20537
+ import { spawn as spawn2 } from "child_process";
20538
+ import { existsSync as existsSync5 } from "fs";
20539
+ import { join as join5, dirname as dirname2 } from "path";
20540
+ import { fileURLToPath as fileURLToPath3 } from "url";
20541
+ import net from "net";
20542
+ var __filename2 = fileURLToPath3(import.meta.url);
20543
+ var __dirname2 = dirname2(__filename2);
20544
+ var OBSERVIO_PORT = 3001;
20545
+ var observioChild = null;
20546
+ function findObservioRoot() {
20547
+ let dir = __dirname2;
20548
+ for (let i = 0; i < 5; i++) {
20549
+ const candidate = join5(dir, "observio-sample-agent");
20550
+ if (existsSync5(join5(candidate, "package.json"))) {
20551
+ return candidate;
20552
+ }
20553
+ dir = dirname2(dir);
20554
+ }
20555
+ return null;
20556
+ }
20557
+ function isPortFree(port) {
20558
+ return new Promise((resolve2) => {
20559
+ let resolved = false;
20560
+ const done = (value) => {
20561
+ if (!resolved) {
20562
+ resolved = true;
20563
+ socket.destroy();
20564
+ resolve2(value);
20565
+ }
20566
+ };
20567
+ const socket = new net.Socket();
20568
+ socket.setTimeout(1e3);
20569
+ socket.once("connect", () => done(false));
20570
+ socket.once("timeout", () => done(true));
20571
+ socket.once("error", () => done(true));
20572
+ socket.connect(port, "localhost");
20573
+ });
20574
+ }
20575
+ function spawnObservioAgent(cwd) {
20576
+ if (!existsSync5(join5(cwd, "node_modules", "@langchain", "langgraph"))) {
20577
+ console.log(" [observio] Dependencies not installed. Run `cd observio-sample-agent && npm install --legacy-peer-deps` first.");
20578
+ return null;
20579
+ }
20580
+ const child = spawn2("npm", ["run", "start:ag-ui"], {
20581
+ cwd,
20582
+ detached: false,
20583
+ stdio: ["ignore", "pipe", "pipe"],
20584
+ shell: true
20585
+ });
20586
+ observioChild = child;
20587
+ child.once("exit", () => {
20588
+ if (observioChild === child) observioChild = null;
20589
+ });
20590
+ child.stdout?.on("data", (data) => {
20591
+ const lines = data.toString().trim().split("\n");
20592
+ for (const line of lines) {
20593
+ if (line.trim()) {
20594
+ console.log(` [observio] ${line}`);
20595
+ }
20596
+ }
20597
+ });
20598
+ child.stderr?.on("data", (data) => {
20599
+ const lines = data.toString().trim().split("\n");
20600
+ for (const line of lines) {
20601
+ if (line.trim()) {
20602
+ console.error(` [observio] ${line}`);
20603
+ }
20604
+ }
20605
+ });
20606
+ return child;
20607
+ }
20608
+
19361
20609
  // server/index.ts
19362
20610
  var PORT2 = config_default.PORT;
20611
+ var MAX_PORT_ATTEMPTS = 10;
20612
+ var observioChild2 = null;
20613
+ async function tryStartObservioAgent() {
20614
+ try {
20615
+ const root = findObservioRoot();
20616
+ if (!root) {
20617
+ console.log(" Observio sample agent: not found (skipped)");
20618
+ return;
20619
+ }
20620
+ const free = await isPortFree(OBSERVIO_PORT);
20621
+ if (!free) {
20622
+ console.log(` Observio sample agent: port ${OBSERVIO_PORT} already in use (skipped)`);
20623
+ return;
20624
+ }
20625
+ observioChild2 = spawnObservioAgent(root);
20626
+ if (!observioChild2) {
20627
+ return;
20628
+ }
20629
+ console.log(` Observio sample agent: starting on port ${OBSERVIO_PORT}...`);
20630
+ observioChild2.on("exit", (code) => {
20631
+ if (code !== null && code !== 0) {
20632
+ console.log(` [observio] Exited with code ${code}`);
20633
+ }
20634
+ observioChild2 = null;
20635
+ });
20636
+ } catch (err) {
20637
+ console.log(` Observio sample agent: failed to start (${err instanceof Error ? err.message : err})`);
20638
+ }
20639
+ }
19363
20640
  async function startServer() {
19364
20641
  const app = await createApp();
19365
20642
  try {
@@ -19369,38 +20646,60 @@ async function startServer() {
19369
20646
  }
19370
20647
  } catch {
19371
20648
  }
19372
- const server = app.listen(PORT2, "0.0.0.0", () => {
19373
- console.log(`
19374
- Backend Server running on http://0.0.0.0:${PORT2}`);
19375
- console.log(` Health check: http://localhost:${PORT2}/health`);
19376
- console.log(` AWS Region: ${process.env.AWS_REGION || "us-west-2"}`);
19377
- console.log(` Bedrock Model: ${process.env.BEDROCK_MODEL_ID || "us.anthropic.claude-sonnet-4-5-20250929-v1:0"}`);
19378
- const storageEndpoint = process.env.OPENSEARCH_STORAGE_ENDPOINT || getStorageConfigFromFile()?.endpoint;
19379
- if (storageEndpoint) {
19380
- console.log(` OpenSearch Storage: ${storageEndpoint}`);
19381
- } else {
19382
- console.log(` OpenSearch Storage: NOT CONFIGURED`);
19383
- }
19384
- console.log("");
19385
- });
19386
- const shutdown = (signal) => {
19387
- console.log(`
20649
+ const tryListen = (port) => {
20650
+ return new Promise((resolve2, reject) => {
20651
+ const server = app.listen(port, "0.0.0.0");
20652
+ server.on("listening", () => {
20653
+ console.log(`
20654
+ Backend Server running on http://0.0.0.0:${port}`);
20655
+ console.log(` Health check: http://localhost:${port}/health`);
20656
+ console.log(` AWS Region: ${process.env.AWS_REGION || "us-west-2"}`);
20657
+ console.log(` Bedrock Model: ${process.env.BEDROCK_MODEL_ID || "us.anthropic.claude-sonnet-4-5-20250929-v1:0"}`);
20658
+ const storageEndpoint = process.env.OPENSEARCH_STORAGE_ENDPOINT || getStorageConfigFromFile()?.endpoint;
20659
+ if (storageEndpoint) {
20660
+ console.log(` OpenSearch Storage: ${storageEndpoint}`);
20661
+ } else {
20662
+ console.log(` OpenSearch Storage: NOT CONFIGURED`);
20663
+ }
20664
+ console.log("");
20665
+ const shutdown = (signal) => {
20666
+ console.log(`
19388
20667
  Received ${signal}, shutting down gracefully...`);
19389
- try {
19390
- const { codingAgentRegistry: codingAgentRegistry3 } = (init_codingAgents(), __toCommonJS(codingAgents_exports));
19391
- if (codingAgentRegistry3) {
19392
- codingAgentRegistry3.stopBackgroundRefresh();
19393
- }
19394
- } catch {
19395
- }
19396
- server.close(() => {
19397
- console.log(" Server closed.");
19398
- process.exit(0);
20668
+ if (observioChild2 && !observioChild2.killed) {
20669
+ console.log(" Stopping observio sample agent...");
20670
+ observioChild2.kill("SIGTERM");
20671
+ observioChild2 = null;
20672
+ }
20673
+ try {
20674
+ const { codingAgentRegistry: codingAgentRegistry3 } = (init_codingAgents(), __toCommonJS(codingAgents_exports));
20675
+ if (codingAgentRegistry3) {
20676
+ codingAgentRegistry3.stopBackgroundRefresh();
20677
+ }
20678
+ } catch {
20679
+ }
20680
+ server.close(() => {
20681
+ console.log(" Server closed.");
20682
+ process.exit(0);
20683
+ });
20684
+ setTimeout(() => process.exit(0), 5e3).unref();
20685
+ };
20686
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
20687
+ process.on("SIGINT", () => shutdown("SIGINT"));
20688
+ tryStartObservioAgent();
20689
+ resolve2();
20690
+ });
20691
+ server.on("error", (err) => {
20692
+ server.close();
20693
+ if (err.code === "EADDRINUSE" && port <= PORT2 + MAX_PORT_ATTEMPTS) {
20694
+ console.log(` Port ${port} is in use, trying ${port + 1}...`);
20695
+ resolve2(tryListen(port + 1));
20696
+ } else {
20697
+ reject(err);
20698
+ }
20699
+ });
19399
20700
  });
19400
- setTimeout(() => process.exit(0), 5e3).unref();
19401
20701
  };
19402
- process.on("SIGTERM", () => shutdown("SIGTERM"));
19403
- process.on("SIGINT", () => shutdown("SIGINT"));
20702
+ await tryListen(PORT2);
19404
20703
  }
19405
20704
  startServer().catch((error) => {
19406
20705
  console.error("Failed to start server:", error);