@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.
@@ -87,6 +87,8 @@ var init_config = __esm({
87
87
  // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
88
88
  openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
89
89
  openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
90
+ // Observio Sample Agent endpoint
91
+ observioEndpoint: getEnvVar("OBSERVIO_ENDPOINT", "http://localhost:3001/run-agent"),
90
92
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
91
93
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
92
94
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -151,6 +153,16 @@ var init_constants = __esm({
151
153
  description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
152
154
  serverOnly: true
153
155
  },
156
+ "strands": {
157
+ label: "Amazon Strands",
158
+ description: "Amazon Strands agent framework via Bedrock Agent Runtime API. Server-only \u2014 requires AWS SDK.",
159
+ serverOnly: true
160
+ },
161
+ "langgraph": {
162
+ label: "LangGraph (REST)",
163
+ description: "LangGraph agent via direct REST API. Use for non-AG-UI LangGraph instances.",
164
+ serverOnly: false
165
+ },
154
166
  "mock": {
155
167
  label: "Mock",
156
168
  description: "Built-in demo agent for testing. No real endpoint needed.",
@@ -170,6 +182,15 @@ var init_constants = __esm({
170
182
  headers: {},
171
183
  useTraces: false
172
184
  },
185
+ {
186
+ key: "observio",
187
+ name: "Observio Sample Agent",
188
+ endpoint: ENV_CONFIG.observioEndpoint || "http://localhost:3001/run-agent",
189
+ description: "Observio sample agent \u2014 ReAct pattern with LangGraph and Bedrock. Start with: cd observio-sample-agent && npm run start:ag-ui",
190
+ connectorType: "agui-streaming",
191
+ headers: {},
192
+ useTraces: true
193
+ },
173
194
  {
174
195
  key: "claude-code",
175
196
  name: "Claude Code",
@@ -179,6 +200,30 @@ var init_constants = __esm({
179
200
  headers: {},
180
201
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
181
202
  connectorConfig: { env: getClaudeCodeConnectorEnv() }
203
+ },
204
+ {
205
+ key: "strands",
206
+ name: "Amazon Strands",
207
+ endpoint: "${STRANDS_AGENT_ID}",
208
+ description: "Amazon Strands agent framework (Bedrock Agent Runtime)",
209
+ connectorType: "strands",
210
+ headers: {},
211
+ useTraces: false,
212
+ connectorConfig: {
213
+ agentAliasId: "${STRANDS_ALIAS_ID:-TSTALIASID}",
214
+ region: "${AWS_REGION:-us-east-1}"
215
+ },
216
+ enabled: false
217
+ },
218
+ {
219
+ key: "langgraph-rest",
220
+ name: "LangGraph (REST)",
221
+ endpoint: "${LANGGRAPH_API_ENDPOINT:-http://localhost:8000}",
222
+ description: "LangGraph agent via direct REST API",
223
+ connectorType: "langgraph",
224
+ headers: {},
225
+ useTraces: false,
226
+ enabled: false
182
227
  }
183
228
  ],
184
229
  models: {
@@ -362,6 +407,7 @@ function mergeConfigs(userConfig, defaultConfig) {
362
407
  ...DEFAULT_SERVER_CONFIG,
363
408
  ...userConfig.server
364
409
  };
410
+ const telemetry = userConfig.telemetry ?? {};
365
411
  return {
366
412
  server,
367
413
  agents,
@@ -369,7 +415,8 @@ function mergeConfigs(userConfig, defaultConfig) {
369
415
  connectors,
370
416
  testCases,
371
417
  reporters,
372
- judge
418
+ judge,
419
+ telemetry
373
420
  };
374
421
  }
375
422
  async function loadUserConfig(configPath) {
@@ -411,7 +458,8 @@ function loadConfigSync(cwd = process.cwd()) {
411
458
  connectors: [],
412
459
  testCases: [],
413
460
  reporters: [["console"]],
414
- judge: { provider: "bedrock", model: "claude-sonnet-4" }
461
+ judge: { provider: "bedrock", model: "claude-sonnet-4" },
462
+ telemetry: {}
415
463
  };
416
464
  }
417
465
  function clearConfigCache() {
@@ -1129,7 +1177,8 @@ var ClaudeCodeReader = class {
1129
1177
  if (s.session_completed) completedSessions++;
1130
1178
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
1131
1179
  totalToolCalls += toolCallCount;
1132
- const date = s.start_time.slice(0, 10);
1180
+ const _d = new Date(s.start_time);
1181
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
1133
1182
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
1134
1183
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
1135
1184
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2017,7 +2066,8 @@ var KiroReader = class {
2017
2066
  if (s.session_completed) completedSessions++;
2018
2067
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
2019
2068
  totalToolCalls += toolCallCount;
2020
- const date = s.start_time.slice(0, 10);
2069
+ const _d = new Date(s.start_time);
2070
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
2021
2071
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2022
2072
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
2023
2073
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2362,7 +2412,8 @@ var CodexReader = class {
2362
2412
  if (s.session_completed) completedSessions++;
2363
2413
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
2364
2414
  totalToolCalls += toolCallCount;
2365
- const date = s.start_time.slice(0, 10);
2415
+ const _d = new Date(s.start_time);
2416
+ const date = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, "0")}-${String(_d.getDate()).padStart(2, "0")}`;
2366
2417
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2367
2418
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
2368
2419
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -2809,7 +2860,7 @@ function localDateStr(d) {
2809
2860
  function filterByDate(sessions, range) {
2810
2861
  if (!range?.from && !range?.to) return sessions;
2811
2862
  return sessions.filter((s) => {
2812
- const date = s.start_time.slice(0, 10);
2863
+ const date = localDateStr(new Date(s.start_time));
2813
2864
  if (range.from && date < range.from) return false;
2814
2865
  if (range.to && date > range.to) return false;
2815
2866
  return true;
@@ -2835,7 +2886,7 @@ function computeStatsFromSessions(sessions, agent) {
2835
2886
  if (s.session_completed) completedSessions++;
2836
2887
  const toolCallCount = Object.values(s.tool_counts).reduce((a, b) => a + b, 0);
2837
2888
  totalToolCalls += toolCallCount;
2838
- const date = s.start_time.slice(0, 10);
2889
+ const date = localDateStr(new Date(s.start_time));
2839
2890
  if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
2840
2891
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
2841
2892
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -3004,7 +3055,7 @@ var CodingAgentRegistry = class {
3004
3055
  }
3005
3056
  const dailyCostMap = /* @__PURE__ */ new Map();
3006
3057
  for (const s of sessions) {
3007
- const date = s.start_time.slice(0, 10);
3058
+ const date = localDateStr(new Date(s.start_time));
3008
3059
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
3009
3060
  const key = `${date}:${s.agent}`;
3010
3061
  const existing = dailyCostMap.get(key) ?? { date, cost: 0, agent: s.agent };
@@ -3032,7 +3083,7 @@ var CodingAgentRegistry = class {
3032
3083
  if (!s.start_time) continue;
3033
3084
  const d = new Date(s.start_time);
3034
3085
  if (isNaN(d.getTime())) continue;
3035
- activeDates.add(s.start_time.slice(0, 10));
3086
+ activeDates.add(localDateStr(new Date(s.start_time)));
3036
3087
  dowCounts[d.getDay()]++;
3037
3088
  hourCounts[d.getHours()]++;
3038
3089
  }
@@ -3059,7 +3110,7 @@ var CodingAgentRegistry = class {
3059
3110
  }
3060
3111
  const dailyMap = /* @__PURE__ */ new Map();
3061
3112
  for (const s of sessions) {
3062
- const date = s.start_time.slice(0, 10);
3113
+ const date = localDateStr(new Date(s.start_time));
3063
3114
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
3064
3115
  const existing = dailyMap.get(date) ?? { date, messageCount: 0, sessionCount: 0, toolCallCount: 0 };
3065
3116
  existing.messageCount += s.user_message_count + s.assistant_message_count;
@@ -3178,7 +3229,7 @@ var CodingAgentRegistry = class {
3178
3229
  const totalDuration = data.sessions.reduce((s, sess) => s + sess.duration_minutes, 0);
3179
3230
  const dailyCostMap = /* @__PURE__ */ new Map();
3180
3231
  for (const s of data.sessions) {
3181
- const date = s.start_time.slice(0, 10);
3232
+ const date = localDateStr(new Date(s.start_time));
3182
3233
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
3183
3234
  const key = `${date}:${s.agent}`;
3184
3235
  const existing = dailyCostMap.get(key) ?? { date, cost: 0, agent: s.agent };
@@ -3387,7 +3438,7 @@ var CodingAgentRegistry = class {
3387
3438
  const totalToolErrors = userSessions.reduce((s, sess) => s + sess.total_tool_errors, 0);
3388
3439
  const totalDuration = userSessions.reduce((s, sess) => s + sess.duration_minutes, 0);
3389
3440
  const agents = [...new Set(userSessions.map((s) => s.agent))];
3390
- const activeDates = new Set(userSessions.map((s) => s.start_time.slice(0, 10)));
3441
+ const activeDates = new Set(userSessions.map((s) => localDateStr(new Date(s.start_time))));
3391
3442
  const projectCounts = /* @__PURE__ */ new Map();
3392
3443
  for (const s of userSessions) {
3393
3444
  projectCounts.set(s.project_path, (projectCounts.get(s.project_path) ?? 0) + 1);
@@ -3495,9 +3546,10 @@ var RemoteAggregator = class extends CodingAgentRegistry {
3495
3546
  filterByRange(sessions, range) {
3496
3547
  if (!range?.from && !range?.to) return sessions;
3497
3548
  return sessions.filter((s) => {
3498
- const t = s.start_time;
3499
- if (range.from && t < range.from) return false;
3500
- if (range.to && t > range.to + "T23:59:59") return false;
3549
+ const y = new Date(s.start_time);
3550
+ const date = `${y.getFullYear()}-${String(y.getMonth() + 1).padStart(2, "0")}-${String(y.getDate()).padStart(2, "0")}`;
3551
+ if (range.from && date < range.from) return false;
3552
+ if (range.to && date > range.to) return false;
3501
3553
  return true;
3502
3554
  });
3503
3555
  }
@@ -4104,15 +4156,15 @@ router2.post("/api/judge", async (req, res) => {
4104
4156
  if (!modelConfig) {
4105
4157
  modelConfig = Object.values(config2.models).find((m) => m.model_id === modelId);
4106
4158
  }
4107
- const provider = modelConfig?.provider || "bedrock";
4159
+ const provider2 = modelConfig?.provider || "bedrock";
4108
4160
  const resolvedModelId = modelConfig?.model_id || modelId;
4109
- debug("JudgeAPI", "Using provider:", provider, "model:", resolvedModelId);
4110
- if (provider === "demo") {
4161
+ debug("JudgeAPI", "Using provider:", provider2, "model:", resolvedModelId);
4162
+ if (provider2 === "demo") {
4111
4163
  debug("JudgeAPI", "Demo provider - returning mock evaluation");
4112
4164
  const mockResult = generateMockEvaluation(trajectory, expectedOutcomes);
4113
4165
  return res.json(mockResult);
4114
4166
  }
4115
- if (provider === "openai-compatible") {
4167
+ if (provider2 === "openai-compatible") {
4116
4168
  debug("JudgeAPI", "OpenAI-compatible provider - calling endpoint");
4117
4169
  const result2 = await evaluateWithOpenAICompatible(
4118
4170
  { trajectory, expectedOutcomes, expectedTrajectory, logs },
@@ -4129,7 +4181,7 @@ router2.post("/api/judge", async (req, res) => {
4129
4181
  res.json(result);
4130
4182
  } catch (error) {
4131
4183
  console.error("[JudgeAPI] Error during evaluation:", error);
4132
- const provider = (() => {
4184
+ const provider2 = (() => {
4133
4185
  try {
4134
4186
  const config2 = loadConfigSync();
4135
4187
  const { modelId } = req.body;
@@ -4139,7 +4191,7 @@ router2.post("/api/judge", async (req, res) => {
4139
4191
  return "bedrock";
4140
4192
  }
4141
4193
  })();
4142
- const errorMessage = provider === "openai-compatible" ? parseOpenAICompatibleError(error) : parseBedrockError(error);
4194
+ const errorMessage = provider2 === "openai-compatible" ? parseOpenAICompatibleError(error) : parseBedrockError(error);
4143
4195
  res.status(500).json({
4144
4196
  error: `Judge evaluation failed: ${errorMessage}`,
4145
4197
  details: error.message
@@ -4430,13 +4482,13 @@ function getCustomAgents() {
4430
4482
  }
4431
4483
 
4432
4484
  // lib/hooks.ts
4433
- async function executeBeforeRequestHook(hooks, context, agentKey) {
4485
+ async function executeBeforeRequestHook(hooks, context2, agentKey) {
4434
4486
  if (!hooks?.beforeRequest) {
4435
- return context;
4487
+ return context2;
4436
4488
  }
4437
4489
  debug("Hooks", `Executing beforeRequest hook for agent "${agentKey}"`);
4438
4490
  try {
4439
- const result = await hooks.beforeRequest(context);
4491
+ const result = await hooks.beforeRequest(context2);
4440
4492
  if (!result || typeof result !== "object") {
4441
4493
  throw new Error("beforeRequest hook must return an object with endpoint, payload, and headers");
4442
4494
  }
@@ -4456,12 +4508,12 @@ async function executeBeforeRequestHook(hooks, context, agentKey) {
4456
4508
  throw new Error(`beforeRequest hook failed for agent "${agentKey}": ${message}`);
4457
4509
  }
4458
4510
  }
4459
- async function executeAfterResponseHook(hooks, context, agentKey) {
4511
+ async function executeAfterResponseHook(hooks, context2, agentKey) {
4460
4512
  if (!hooks?.afterResponse) {
4461
- return context;
4513
+ return context2;
4462
4514
  }
4463
4515
  try {
4464
- const result = await hooks.afterResponse(context);
4516
+ const result = await hooks.afterResponse(context2);
4465
4517
  if (!result || typeof result !== "object") {
4466
4518
  throw new Error("afterResponse hook must return an object with response, trajectory, and optional runId");
4467
4519
  }
@@ -4477,12 +4529,12 @@ async function executeAfterResponseHook(hooks, context, agentKey) {
4477
4529
  throw new Error(`afterResponse hook failed for agent "${agentKey}": ${message}`);
4478
4530
  }
4479
4531
  }
4480
- async function executeBuildTrajectoryHook(hooks, context, agentKey) {
4532
+ async function executeBuildTrajectoryHook(hooks, context2, agentKey) {
4481
4533
  if (!hooks?.buildTrajectory) {
4482
4534
  return null;
4483
4535
  }
4484
4536
  try {
4485
- const result = await hooks.buildTrajectory(context);
4537
+ const result = await hooks.buildTrajectory(context2);
4486
4538
  if (result !== null && !Array.isArray(result)) {
4487
4539
  throw new Error("buildTrajectory hook must return an array of TrajectoryStep objects or null");
4488
4540
  }
@@ -4533,6 +4585,36 @@ var agent_default = router3;
4533
4585
  import { Router as Router4 } from "express";
4534
4586
 
4535
4587
  // server/services/tracesService.ts
4588
+ function classifyOpenSearchError(error) {
4589
+ const statusCode = error.meta?.statusCode;
4590
+ const message = error.message || "Unknown error";
4591
+ if (statusCode === 401 || statusCode === 403) {
4592
+ return {
4593
+ category: "auth",
4594
+ message: `Authentication failed (HTTP ${statusCode})`,
4595
+ 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."
4596
+ };
4597
+ }
4598
+ if (statusCode === 404) {
4599
+ return {
4600
+ category: "index_not_found",
4601
+ message: "Trace index not found",
4602
+ suggestion: "The OpenSearch traces index does not exist yet. Ensure your agents are configured to emit OpenTelemetry spans."
4603
+ };
4604
+ }
4605
+ if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("getaddrinfo") || message.includes("Connection refused")) {
4606
+ return {
4607
+ category: "connection",
4608
+ message: `Cannot connect to OpenSearch: ${message}`,
4609
+ suggestion: "Check that the OpenSearch endpoint is correct and the cluster is reachable from your network."
4610
+ };
4611
+ }
4612
+ return {
4613
+ category: "unknown",
4614
+ message,
4615
+ suggestion: "Check the server logs for more details."
4616
+ };
4617
+ }
4536
4618
  function transformSpan(source) {
4537
4619
  const attributes = {};
4538
4620
  for (const [key, value] of Object.entries(source)) {
@@ -4587,11 +4669,11 @@ function transformSpan(source) {
4587
4669
  };
4588
4670
  }
4589
4671
  async function fetchTraces(options, client, indexPattern = "otel-v1-apm-span-*") {
4590
- const { traceId, runIds, startTime, endTime, size = 100, serviceName, textSearch, cursor } = options;
4672
+ const { traceId, runIds, sessionId, startTime, endTime, size = 100, serviceName, textSearch, cursor } = options;
4591
4673
  const hasTimeRange = startTime || endTime;
4592
- const hasIdFilter = traceId || runIds && runIds.length > 0;
4674
+ const hasIdFilter = traceId || runIds && runIds.length > 0 || sessionId;
4593
4675
  if (!hasIdFilter && !hasTimeRange) {
4594
- throw new Error("Either traceId, runIds, or time range is required");
4676
+ throw new Error("Either traceId, runIds, sessionId, or time range is required");
4595
4677
  }
4596
4678
  debug("TracesService", "Fetching traces:", { traceId, runIds: runIds?.length, serviceName, textSearch, size, cursor: cursor ? "present" : "none" });
4597
4679
  const must = [];
@@ -4603,6 +4685,9 @@ async function fetchTraces(options, client, indexPattern = "otel-v1-apm-span-*")
4603
4685
  terms: { "span.attributes.gen_ai@request@id": runIds }
4604
4686
  });
4605
4687
  }
4688
+ if (sessionId) {
4689
+ must.push({ term: { "span.attributes.session@id": sessionId } });
4690
+ }
4606
4691
  if (startTime || endTime) {
4607
4692
  const range = { "startTime": {} };
4608
4693
  if (startTime) range["startTime"].gte = new Date(startTime).toISOString();
@@ -4674,7 +4759,13 @@ async function checkTracesHealth(client, indexPattern = "otel-v1-apm-span-*") {
4674
4759
  return { status: "error", index: indexPattern };
4675
4760
  }
4676
4761
  } catch (error) {
4677
- return { status: "error", error: error.message };
4762
+ const classified = classifyOpenSearchError(error);
4763
+ return {
4764
+ status: "error",
4765
+ error: classified.message,
4766
+ errorCategory: classified.category,
4767
+ suggestion: classified.suggestion
4768
+ };
4678
4769
  }
4679
4770
  }
4680
4771
 
@@ -6328,10 +6419,10 @@ function createOpenSearchClient(config2) {
6328
6419
  region: config2.awsRegion,
6329
6420
  service: config2.awsService || "es",
6330
6421
  getCredentials: () => {
6331
- const provider = fromNodeProviderChain2({
6422
+ const provider2 = fromNodeProviderChain2({
6332
6423
  ...config2.awsProfile && { profile: config2.awsProfile }
6333
6424
  });
6334
- return provider();
6425
+ return provider2();
6335
6426
  }
6336
6427
  });
6337
6428
  return new Client({
@@ -7252,15 +7343,19 @@ var OpenSearchRunOperations = class {
7252
7343
  async getAll(options) {
7253
7344
  const size = options?.size ?? 100;
7254
7345
  const from = options?.from ?? 0;
7346
+ const body = {
7347
+ size,
7348
+ from,
7349
+ sort: [{ createdAt: { order: "desc" } }],
7350
+ query: { match_all: {} }
7351
+ };
7352
+ if (options?._source && options._source.length > 0) {
7353
+ body._source = options._source;
7354
+ }
7255
7355
  try {
7256
7356
  const result = await this.client.search({
7257
7357
  index: this.index,
7258
- body: {
7259
- size,
7260
- from,
7261
- sort: [{ createdAt: { order: "desc" } }],
7262
- query: { match_all: {} }
7263
- }
7358
+ body
7264
7359
  });
7265
7360
  const items = hitsToSources(result.body.hits?.hits || []);
7266
7361
  const total = typeof result.body.hits?.total === "object" ? result.body.hits.total.value : result.body.hits?.total ?? 0;
@@ -7964,8 +8059,12 @@ function resolveObservabilityConfig(req) {
7964
8059
  if (fileConfig) {
7965
8060
  return {
7966
8061
  endpoint: fileConfig.endpoint,
8062
+ authType: fileConfig.authType,
7967
8063
  username: fileConfig.username,
7968
8064
  password: fileConfig.password,
8065
+ awsProfile: fileConfig.awsProfile,
8066
+ awsRegion: fileConfig.awsRegion,
8067
+ awsService: fileConfig.awsService,
7969
8068
  tlsSkipVerify: fileConfig.tlsSkipVerify,
7970
8069
  indexes: {
7971
8070
  traces: fileConfig.indexes?.traces || DEFAULT_OTEL_INDEXES.traces,
@@ -8009,14 +8108,34 @@ function getStorageConfigFromEnv() {
8009
8108
  awsService: process.env.OPENSEARCH_STORAGE_AWS_SERVICE || void 0
8010
8109
  };
8011
8110
  }
8111
+ function getObservabilityConfigFromEnv() {
8112
+ const endpoint = process.env.OPENSEARCH_LOGS_ENDPOINT;
8113
+ if (!endpoint) {
8114
+ return null;
8115
+ }
8116
+ return {
8117
+ endpoint,
8118
+ authType: process.env.OPENSEARCH_LOGS_AUTH_TYPE || void 0,
8119
+ username: process.env.OPENSEARCH_LOGS_USERNAME,
8120
+ password: process.env.OPENSEARCH_LOGS_PASSWORD,
8121
+ awsProfile: process.env.OPENSEARCH_LOGS_AWS_PROFILE,
8122
+ awsRegion: process.env.OPENSEARCH_LOGS_AWS_REGION,
8123
+ awsService: process.env.OPENSEARCH_LOGS_AWS_SERVICE || void 0,
8124
+ indexes: {
8125
+ traces: process.env.OPENSEARCH_LOGS_TRACES_INDEX || DEFAULT_OTEL_INDEXES.traces,
8126
+ logs: process.env.OPENSEARCH_LOGS_INDEX || DEFAULT_OTEL_INDEXES.logs,
8127
+ metrics: DEFAULT_OTEL_INDEXES.metrics
8128
+ }
8129
+ };
8130
+ }
8012
8131
 
8013
8132
  // server/routes/traces.ts
8014
8133
  var router4 = Router4();
8015
8134
  router4.post("/api/traces", async (req, res) => {
8016
8135
  try {
8017
- const { traceId, runIds, startTime, endTime, size = 100, serviceName, textSearch, cursor } = req.body;
8136
+ const { traceId, runIds, sessionId, startTime, endTime, size = 100, serviceName, textSearch, cursor } = req.body;
8018
8137
  const hasTimeRange = startTime || endTime;
8019
- const hasIdFilter = traceId || runIds && runIds.length > 0;
8138
+ const hasIdFilter = traceId || runIds && runIds.length > 0 || sessionId;
8020
8139
  if (!hasIdFilter && !hasTimeRange) {
8021
8140
  return res.status(400).json({
8022
8141
  error: "Either traceId, runIds, or time range is required"
@@ -8030,16 +8149,18 @@ router4.post("/api/traces", async (req, res) => {
8030
8149
  }
8031
8150
  let realSpans = [];
8032
8151
  let warning;
8152
+ let warningCategory;
8153
+ let suggestion;
8033
8154
  let nextCursor = null;
8034
8155
  let hasMore = false;
8035
8156
  const config2 = resolveObservabilityConfig(req);
8036
- if (config2 && (traceId || runIds && runIds.length > 0 || startTime || endTime)) {
8157
+ if (config2 && (traceId || runIds && runIds.length > 0 || sessionId || startTime || endTime)) {
8037
8158
  let client;
8038
8159
  try {
8039
8160
  client = createOpenSearchClient(config2);
8040
8161
  const indexPattern = config2.indexes?.traces || DEFAULT_OTEL_INDEXES.traces;
8041
8162
  const result = await fetchTraces(
8042
- { traceId, runIds, startTime, endTime, size, serviceName, textSearch, cursor },
8163
+ { traceId, runIds, sessionId, startTime, endTime, size, serviceName, textSearch, cursor },
8043
8164
  client,
8044
8165
  indexPattern
8045
8166
  );
@@ -8047,8 +8168,11 @@ router4.post("/api/traces", async (req, res) => {
8047
8168
  nextCursor = result.nextCursor || null;
8048
8169
  hasMore = result.hasMore || false;
8049
8170
  } catch (e) {
8050
- console.warn("[TracesAPI] OpenSearch query failed:", e.message);
8051
- warning = e.message;
8171
+ const classified = classifyOpenSearchError(e);
8172
+ console.warn(`[TracesAPI] OpenSearch query failed (${classified.category}):`, classified.message);
8173
+ warning = classified.message;
8174
+ warningCategory = classified.category;
8175
+ suggestion = classified.suggestion;
8052
8176
  } finally {
8053
8177
  if (client) {
8054
8178
  await client.close().catch(() => {
@@ -8074,6 +8198,8 @@ router4.post("/api/traces", async (req, res) => {
8074
8198
  }
8075
8199
  }
8076
8200
  warning = "Observability data source not configured";
8201
+ warningCategory = "not_configured";
8202
+ suggestion = "Configure OPENSEARCH_LOGS_ENDPOINT or observabilityStorage in agent-health.config.ts to connect to your traces cluster.";
8077
8203
  }
8078
8204
  const allSpans = [...sampleSpans, ...realSpans];
8079
8205
  res.json({
@@ -8081,7 +8207,9 @@ router4.post("/api/traces", async (req, res) => {
8081
8207
  total: allSpans.length,
8082
8208
  nextCursor,
8083
8209
  hasMore,
8084
- warning
8210
+ warning,
8211
+ warningCategory,
8212
+ suggestion
8085
8213
  });
8086
8214
  } catch (error) {
8087
8215
  console.error("[TracesAPI] Error:", error);
@@ -8198,33 +8326,21 @@ function computeMetricsFromSampleSpans(runId) {
8198
8326
  status: "success"
8199
8327
  };
8200
8328
  }
8201
- async function computeMetrics(runId, osConfig) {
8202
- const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8203
- const query = {
8204
- size: 500,
8205
- sort: [{ startTime: { order: "asc" } }],
8206
- query: {
8207
- bool: {
8208
- must: [
8209
- { term: { "span.attributes.gen_ai@request@id": runId } }
8210
- ]
8211
- }
8212
- }
8213
- };
8214
- const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8215
- method: "POST",
8216
- headers: {
8217
- "Content-Type": "application/json",
8218
- "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8219
- },
8220
- body: JSON.stringify(query)
8221
- });
8222
- if (!response.ok) {
8223
- const errorText = await response.text();
8224
- throw new Error(`OpenSearch query failed: ${response.status} - ${errorText}`);
8225
- }
8226
- const data = await response.json();
8227
- const spans = data.hits?.hits?.map((h) => h._source) || [];
8329
+ var METRICS_SOURCE_FIELDS = [
8330
+ "span.attributes.gen_ai@request@id",
8331
+ "span.attributes.gen_ai@usage@input_tokens",
8332
+ "span.attributes.gen_ai@usage@output_tokens",
8333
+ "span.attributes.gen_ai@request@model",
8334
+ "span.attributes.gen_ai@tool@name",
8335
+ "span.attributes.tool.name",
8336
+ "name",
8337
+ "traceId",
8338
+ "startTime",
8339
+ "endTime",
8340
+ "durationInNanos",
8341
+ "status.code"
8342
+ ];
8343
+ function computeMetricsFromSpans(runId, spans) {
8228
8344
  if (spans.length === 0) {
8229
8345
  return {
8230
8346
  runId,
@@ -8296,6 +8412,95 @@ async function computeMetrics(runId, osConfig) {
8296
8412
  status
8297
8413
  };
8298
8414
  }
8415
+ async function computeMetrics(runId, osConfig) {
8416
+ const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8417
+ const query = {
8418
+ size: 500,
8419
+ sort: [{ startTime: { order: "asc" } }],
8420
+ query: {
8421
+ bool: {
8422
+ must: [
8423
+ { term: { "span.attributes.gen_ai@request@id": runId } }
8424
+ ]
8425
+ }
8426
+ }
8427
+ };
8428
+ const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8429
+ method: "POST",
8430
+ headers: {
8431
+ "Content-Type": "application/json",
8432
+ "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8433
+ },
8434
+ body: JSON.stringify(query)
8435
+ });
8436
+ if (!response.ok) {
8437
+ const errorText = await response.text();
8438
+ throw new Error(`OpenSearch query failed: ${response.status} - ${errorText}`);
8439
+ }
8440
+ const data = await response.json();
8441
+ const spans = data.hits?.hits?.map((h) => h._source) || [];
8442
+ return computeMetricsFromSpans(runId, spans);
8443
+ }
8444
+ async function computeBatchMetrics(runIds, osConfig) {
8445
+ if (runIds.length === 0) return [];
8446
+ const { endpoint, username, password, indexPattern = "otel-v1-apm-span-*" } = osConfig;
8447
+ const CHUNK_SIZE = 50;
8448
+ const allResults = [];
8449
+ const chunks = [];
8450
+ for (let i = 0; i < runIds.length; i += CHUNK_SIZE) {
8451
+ chunks.push(runIds.slice(i, i + CHUNK_SIZE));
8452
+ }
8453
+ const chunkResults = await Promise.all(chunks.map(async (chunk) => {
8454
+ const query = {
8455
+ size: 1e4,
8456
+ sort: [{ startTime: { order: "asc" } }],
8457
+ _source: METRICS_SOURCE_FIELDS,
8458
+ query: {
8459
+ bool: {
8460
+ must: [
8461
+ { terms: { "span.attributes.gen_ai@request@id": chunk } }
8462
+ ]
8463
+ }
8464
+ }
8465
+ };
8466
+ const response = await fetch(`${endpoint}/${indexPattern}/_search`, {
8467
+ method: "POST",
8468
+ headers: {
8469
+ "Content-Type": "application/json",
8470
+ "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
8471
+ },
8472
+ body: JSON.stringify(query)
8473
+ });
8474
+ if (!response.ok) {
8475
+ const responseBody = await response.text();
8476
+ console.warn(
8477
+ `OpenSearch metrics query failed for chunk (${chunk.length} run IDs): ${response.status} ${response.statusText}. Response body: ${responseBody}`
8478
+ );
8479
+ return chunk.map((runId) => computeMetricsFromSpans(runId, []));
8480
+ }
8481
+ const data = await response.json();
8482
+ const allSpans = data.hits?.hits?.map((h) => h._source) || [];
8483
+ const totalHits = data.hits?.total?.value ?? allSpans.length;
8484
+ if (totalHits > 1e4) {
8485
+ console.warn(
8486
+ `OpenSearch batch metrics query returned ${allSpans.length} of ${totalHits} spans for chunk of ${chunk.length} run IDs. Metrics may be incomplete.`
8487
+ );
8488
+ }
8489
+ const spansByRunId = /* @__PURE__ */ new Map();
8490
+ for (const rid of chunk) spansByRunId.set(rid, []);
8491
+ for (const span of allSpans) {
8492
+ const rid = span["span.attributes.gen_ai@request@id"];
8493
+ if (rid && spansByRunId.has(rid)) {
8494
+ spansByRunId.get(rid).push(span);
8495
+ }
8496
+ }
8497
+ return chunk.map((runId) => computeMetricsFromSpans(runId, spansByRunId.get(runId) || []));
8498
+ }));
8499
+ for (const results of chunkResults) {
8500
+ allResults.push(...results);
8501
+ }
8502
+ return allResults;
8503
+ }
8299
8504
  function computeAggregateMetrics(metricsArray) {
8300
8505
  if (!metricsArray || metricsArray.length === 0) {
8301
8506
  return {
@@ -8374,15 +8579,6 @@ router5.get("/api/metrics/:runId", async (req, res) => {
8374
8579
  res.status(500).json({ error: error.message });
8375
8580
  }
8376
8581
  });
8377
- async function processBatches(items, batchSize, processor) {
8378
- const results = [];
8379
- for (let i = 0; i < items.length; i += batchSize) {
8380
- const batch = items.slice(i, i + batchSize);
8381
- const batchResults = await Promise.all(batch.map(processor));
8382
- results.push(...batchResults);
8383
- }
8384
- return results;
8385
- }
8386
8582
  router5.post("/api/metrics/batch", async (req, res) => {
8387
8583
  try {
8388
8584
  const { runIds } = req.body;
@@ -8413,16 +8609,15 @@ router5.post("/api/metrics/batch", async (req, res) => {
8413
8609
  password: config2.password,
8414
8610
  indexPattern
8415
8611
  };
8416
- const BATCH_SIZE = 10;
8417
- realResults = await processBatches(
8418
- realRunIds,
8419
- BATCH_SIZE,
8420
- (runId) => computeMetrics(runId, osConfig).catch((e) => ({
8612
+ try {
8613
+ realResults = await computeBatchMetrics(realRunIds, osConfig);
8614
+ } catch (e) {
8615
+ realResults = realRunIds.map((runId) => ({
8421
8616
  runId,
8422
8617
  error: e.message,
8423
8618
  status: "error"
8424
- }))
8425
- );
8619
+ }));
8620
+ }
8426
8621
  }
8427
8622
  }
8428
8623
  const resultsMap = /* @__PURE__ */ new Map();
@@ -12092,11 +12287,150 @@ var OpenAICompatibleConnector = class extends BaseConnector {
12092
12287
  };
12093
12288
  var openaiCompatibleConnector = new OpenAICompatibleConnector();
12094
12289
 
12290
+ // services/connectors/langgraph/LangGraphConnector.ts
12291
+ var LangGraphConnector = class extends BaseConnector {
12292
+ constructor() {
12293
+ super(...arguments);
12294
+ this.type = "langgraph";
12295
+ this.name = "LangGraph (REST)";
12296
+ this.supportsStreaming = false;
12297
+ }
12298
+ buildPayload(request2) {
12299
+ const config2 = request2.connectorConfig || {};
12300
+ return {
12301
+ input: {
12302
+ messages: [
12303
+ {
12304
+ role: "user",
12305
+ content: request2.testCase.initialPrompt
12306
+ }
12307
+ ]
12308
+ },
12309
+ config: {
12310
+ configurable: {
12311
+ ...request2.modelId && { model: request2.modelId },
12312
+ ...config2.configurable
12313
+ }
12314
+ }
12315
+ };
12316
+ }
12317
+ async execute(endpoint, request2, auth, onProgress, onRawEvent) {
12318
+ const payload = request2.payload || this.buildPayload(request2);
12319
+ const headers = this.buildAuthHeaders(auth);
12320
+ const config2 = request2.connectorConfig || {};
12321
+ const graphId = config2.graphId || "agent";
12322
+ const baseUrl = endpoint.replace(/\/+$/, "");
12323
+ const threadId = config2.threadId || request2.threadId;
12324
+ const invokeUrl = threadId ? `${baseUrl}/threads/${threadId}/runs/wait` : `${baseUrl}/assistants/${graphId}/invoke`;
12325
+ this.debug("Executing LangGraph request");
12326
+ this.debug("URL:", invokeUrl);
12327
+ const response = await fetch(invokeUrl, {
12328
+ method: "POST",
12329
+ headers: {
12330
+ "Content-Type": "application/json",
12331
+ ...headers
12332
+ },
12333
+ body: JSON.stringify(payload)
12334
+ });
12335
+ if (!response.ok) {
12336
+ const errorText = await response.text();
12337
+ throw new Error(`LangGraph request failed: ${response.status} - ${errorText}`);
12338
+ }
12339
+ const data = await response.json();
12340
+ onRawEvent?.(data);
12341
+ const trajectory = this.parseResponse(data);
12342
+ trajectory.forEach((step) => onProgress?.(step));
12343
+ return {
12344
+ trajectory,
12345
+ runId: data.run_id || data.thread_id || threadId || null,
12346
+ rawEvents: [data],
12347
+ metadata: {
12348
+ graphId,
12349
+ threadId: data.thread_id || threadId
12350
+ }
12351
+ };
12352
+ }
12353
+ parseResponse(data) {
12354
+ const steps = [];
12355
+ const messages = data.output?.messages || data.values?.messages || data.messages || [];
12356
+ for (const msg of messages) {
12357
+ const role = msg.type || msg.role;
12358
+ 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);
12359
+ if (role === "ai" || role === "assistant") {
12360
+ if (msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
12361
+ for (const toolCall of msg.tool_calls) {
12362
+ steps.push(this.createStep("action", `Calling ${toolCall.name}...`, {
12363
+ toolName: toolCall.name,
12364
+ toolArgs: toolCall.args
12365
+ }));
12366
+ }
12367
+ }
12368
+ if (content && !msg.tool_calls?.length) {
12369
+ steps.push(this.createStep("assistant", content));
12370
+ }
12371
+ } else if (role === "tool") {
12372
+ steps.push(this.createStep("tool_result", content, {
12373
+ toolName: msg.name,
12374
+ status: "SUCCESS"
12375
+ }));
12376
+ }
12377
+ }
12378
+ const lastAssistant = [...steps].reverse().find((s) => s.type === "assistant");
12379
+ if (lastAssistant) {
12380
+ lastAssistant.type = "response";
12381
+ }
12382
+ if (data.intermediate_steps && Array.isArray(data.intermediate_steps)) {
12383
+ for (const [action, observation] of data.intermediate_steps) {
12384
+ if (action?.tool) {
12385
+ steps.push(this.createStep("action", `Calling ${action.tool}...`, {
12386
+ toolName: action.tool,
12387
+ toolArgs: action.tool_input
12388
+ }));
12389
+ }
12390
+ if (observation !== void 0) {
12391
+ const obsContent = typeof observation === "string" ? observation : JSON.stringify(observation);
12392
+ steps.push(this.createStep("tool_result", obsContent, {
12393
+ status: "SUCCESS"
12394
+ }));
12395
+ }
12396
+ }
12397
+ }
12398
+ if (data.output && typeof data.output === "string" && steps.length === 0) {
12399
+ steps.push(this.createStep("response", data.output));
12400
+ }
12401
+ if (steps.length === 0 && data) {
12402
+ steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
12403
+ }
12404
+ return steps;
12405
+ }
12406
+ async healthCheck(endpoint, auth) {
12407
+ try {
12408
+ const headers = this.buildAuthHeaders(auth);
12409
+ const baseUrl = endpoint.replace(/\/+$/, "");
12410
+ const response = await fetch(`${baseUrl}/ok`, {
12411
+ method: "GET",
12412
+ headers
12413
+ });
12414
+ return response.ok;
12415
+ } catch {
12416
+ try {
12417
+ const headers = this.buildAuthHeaders(auth);
12418
+ const response = await fetch(endpoint, { method: "GET", headers });
12419
+ return response.ok;
12420
+ } catch {
12421
+ return false;
12422
+ }
12423
+ }
12424
+ }
12425
+ };
12426
+ var langgraphConnector = new LangGraphConnector();
12427
+
12095
12428
  // services/connectors/index.ts
12096
12429
  connectorRegistry.register(aguiStreamingConnector);
12097
12430
  connectorRegistry.register(mockConnector);
12098
12431
  connectorRegistry.register(restConnector);
12099
12432
  connectorRegistry.register(openaiCompatibleConnector);
12433
+ connectorRegistry.register(langgraphConnector);
12100
12434
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
12101
12435
 
12102
12436
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -12140,6 +12474,7 @@ Question: ${prompt}`;
12140
12474
  const command = endpoint || this.config.command;
12141
12475
  const args = this.config.args || [];
12142
12476
  const input = request2.payload || this.buildPayload(request2);
12477
+ const runId = `subprocess-${Date.now()}`;
12143
12478
  this.debug("Command:", command);
12144
12479
  this.debug("Args:", args);
12145
12480
  this.debug("Input mode:", this.config.inputMode);
@@ -12147,10 +12482,12 @@ Question: ${prompt}`;
12147
12482
  this.debug("Timeout:", this.config.timeout);
12148
12483
  this.debug("Input (first 500 chars):", input.substring(0, 500));
12149
12484
  this.debug("Working dir:", this.config.workingDir || process.cwd());
12485
+ this.debug("Run ID:", runId);
12150
12486
  const env = {
12151
12487
  ...process.env,
12152
12488
  ...this.buildAuthEnv(auth),
12153
- ...this.config.env
12489
+ ...this.config.env,
12490
+ AGENT_EVAL_RUN_ID: runId
12154
12491
  };
12155
12492
  return new Promise((resolve2, reject) => {
12156
12493
  const trajectory = [];
@@ -12209,6 +12546,17 @@ Question: ${prompt}`;
12209
12546
  this.error(`Process exited with code ${code}`);
12210
12547
  this.error("stderr:", stderr);
12211
12548
  }
12549
+ if (this.config.outputParser === "streaming") {
12550
+ this.onBeforeStreamEnd(trajectory, onProgress);
12551
+ if (code !== 0 && trajectory.length === 0) {
12552
+ const errorContent = stderr.trim() ? `Error: Process exited with code ${code}. ${stderr.trim()}` : `Error: Process exited with code ${code}`;
12553
+ const errorStep = this.createStep("tool_result", errorContent, {
12554
+ status: "FAILURE" /* FAILURE */
12555
+ });
12556
+ trajectory.push(errorStep);
12557
+ onProgress?.(errorStep);
12558
+ }
12559
+ }
12212
12560
  const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code });
12213
12561
  if (this.config.outputParser !== "streaming") {
12214
12562
  finalTrajectory.forEach((step) => onProgress?.(step));
@@ -12216,7 +12564,7 @@ Question: ${prompt}`;
12216
12564
  this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
12217
12565
  resolve2({
12218
12566
  trajectory: finalTrajectory,
12219
- runId: `subprocess-${Date.now()}`,
12567
+ runId,
12220
12568
  rawEvents: rawOutput,
12221
12569
  metadata: {
12222
12570
  command,
@@ -12247,6 +12595,12 @@ Question: ${prompt}`;
12247
12595
  });
12248
12596
  this.debug("========== execute() COMPLETED ==========");
12249
12597
  }
12598
+ /**
12599
+ * Hook called before returning the streaming trajectory on process close.
12600
+ * Subclasses can override to flush internal buffers.
12601
+ */
12602
+ onBeforeStreamEnd(_trajectory, _onProgress) {
12603
+ }
12250
12604
  /**
12251
12605
  * Parse streaming output and emit steps in real-time
12252
12606
  */
@@ -12341,6 +12695,7 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12341
12695
  this.name = "Claude Code CLI";
12342
12696
  this.outputBuffer = "";
12343
12697
  this.thinkingBuffer = "";
12698
+ this.textBuffer = "";
12344
12699
  this.isInThinking = false;
12345
12700
  }
12346
12701
  /**
@@ -12410,11 +12765,17 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12410
12765
  if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
12411
12766
  this.thinkingBuffer += event.delta.thinking;
12412
12767
  } else if (event.delta?.type === "text_delta" && event.delta.text) {
12413
- steps.push(this.createStep("assistant", event.delta.text));
12768
+ this.textBuffer += event.delta.text;
12769
+ }
12770
+ } else if (event.type === "content_block_stop") {
12771
+ if (this.thinkingBuffer) {
12772
+ steps.push(this.createStep("thinking", this.thinkingBuffer));
12773
+ this.thinkingBuffer = "";
12774
+ }
12775
+ if (this.textBuffer) {
12776
+ steps.push(this.createStep("assistant", this.textBuffer));
12777
+ this.textBuffer = "";
12414
12778
  }
12415
- } else if (event.type === "content_block_stop" && this.thinkingBuffer) {
12416
- steps.push(this.createStep("thinking", this.thinkingBuffer));
12417
- this.thinkingBuffer = "";
12418
12779
  } else if (event.type === "result" && event.result) {
12419
12780
  steps.push(this.createStep(
12420
12781
  "response",
@@ -12460,8 +12821,41 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12460
12821
  resetState() {
12461
12822
  this.outputBuffer = "";
12462
12823
  this.thinkingBuffer = "";
12824
+ this.textBuffer = "";
12463
12825
  this.isInThinking = false;
12464
12826
  }
12827
+ /**
12828
+ * Flush remaining buffers when the subprocess stream ends.
12829
+ */
12830
+ onBeforeStreamEnd(trajectory, onProgress) {
12831
+ if (this.outputBuffer.trim()) {
12832
+ try {
12833
+ const event = JSON.parse(this.outputBuffer.trim());
12834
+ const steps = this.parseJsonEvent(event);
12835
+ for (const step of steps) {
12836
+ trajectory.push(step);
12837
+ onProgress?.(step);
12838
+ }
12839
+ } catch {
12840
+ const step = this.createStep("assistant", this.outputBuffer.trim());
12841
+ trajectory.push(step);
12842
+ onProgress?.(step);
12843
+ }
12844
+ this.outputBuffer = "";
12845
+ }
12846
+ if (this.thinkingBuffer) {
12847
+ const step = this.createStep("thinking", this.thinkingBuffer);
12848
+ trajectory.push(step);
12849
+ onProgress?.(step);
12850
+ this.thinkingBuffer = "";
12851
+ }
12852
+ if (this.textBuffer) {
12853
+ const step = this.createStep("response", this.textBuffer);
12854
+ trajectory.push(step);
12855
+ onProgress?.(step);
12856
+ this.textBuffer = "";
12857
+ }
12858
+ }
12465
12859
  /**
12466
12860
  * Build CLI args from ClaudeCodeConnectorConfig
12467
12861
  */
@@ -12562,9 +12956,214 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
12562
12956
  };
12563
12957
  var claudeCodeConnector = new ClaudeCodeConnector();
12564
12958
 
12959
+ // services/connectors/strands/StrandsConnector.ts
12960
+ var StrandsConnector = class extends BaseConnector {
12961
+ constructor() {
12962
+ super(...arguments);
12963
+ this.type = "strands";
12964
+ this.name = "Amazon Strands";
12965
+ this.supportsStreaming = true;
12966
+ }
12967
+ buildPayload(request2) {
12968
+ const config2 = request2.connectorConfig || {};
12969
+ return {
12970
+ agentId: "",
12971
+ // Set from endpoint in execute()
12972
+ agentAliasId: config2.agentAliasId || "TSTALIASID",
12973
+ sessionId: config2.sessionId || `eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
12974
+ inputText: request2.testCase.initialPrompt,
12975
+ enableTrace: config2.enableTrace !== false
12976
+ };
12977
+ }
12978
+ async execute(endpoint, request2, auth, onProgress, onRawEvent) {
12979
+ const payload = request2.payload || this.buildPayload(request2);
12980
+ payload.agentId = endpoint;
12981
+ const config2 = request2.connectorConfig || {};
12982
+ const region = config2.region || auth.awsRegion || process.env.AWS_REGION || "us-east-1";
12983
+ this.debug("Executing Strands agent");
12984
+ this.debug("Agent ID:", endpoint);
12985
+ this.debug("Alias:", payload.agentAliasId);
12986
+ this.debug("Region:", region);
12987
+ const { BedrockAgentRuntimeClient, InvokeAgentCommand } = await import("@aws-sdk/client-bedrock-agent-runtime");
12988
+ const clientConfig = { region };
12989
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
12990
+ clientConfig.credentials = {
12991
+ accessKeyId: auth.awsAccessKeyId,
12992
+ secretAccessKey: auth.awsSecretAccessKey,
12993
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
12994
+ };
12995
+ }
12996
+ const client = new BedrockAgentRuntimeClient(clientConfig);
12997
+ const command = new InvokeAgentCommand({
12998
+ agentId: payload.agentId,
12999
+ agentAliasId: payload.agentAliasId,
13000
+ sessionId: payload.sessionId,
13001
+ inputText: payload.inputText,
13002
+ enableTrace: payload.enableTrace
13003
+ });
13004
+ const response = await client.send(command);
13005
+ const trajectory = [];
13006
+ let finalOutput = "";
13007
+ if (response.completion) {
13008
+ for await (const event of response.completion) {
13009
+ onRawEvent?.(event);
13010
+ if (event.chunk?.bytes) {
13011
+ const text = new TextDecoder().decode(event.chunk.bytes);
13012
+ finalOutput += text;
13013
+ }
13014
+ if (event.trace?.trace) {
13015
+ const traceSteps = this.parseTraceEvent(event.trace.trace);
13016
+ for (const step of traceSteps) {
13017
+ trajectory.push(step);
13018
+ onProgress?.(step);
13019
+ }
13020
+ }
13021
+ }
13022
+ }
13023
+ const hasResponseFromTrace = trajectory.some((s) => s.type === "response");
13024
+ if (finalOutput && !hasResponseFromTrace) {
13025
+ const responseStep = this.createStep("response", finalOutput);
13026
+ trajectory.push(responseStep);
13027
+ onProgress?.(responseStep);
13028
+ }
13029
+ return {
13030
+ trajectory,
13031
+ runId: payload.sessionId,
13032
+ metadata: {
13033
+ agentId: endpoint,
13034
+ agentAliasId: payload.agentAliasId,
13035
+ sessionId: payload.sessionId,
13036
+ region
13037
+ }
13038
+ };
13039
+ }
13040
+ parseResponse(rawResponse) {
13041
+ if (rawResponse?.trace?.trace) {
13042
+ return this.parseTraceEvent(rawResponse.trace.trace);
13043
+ }
13044
+ if (typeof rawResponse === "string") {
13045
+ return [this.createStep("response", rawResponse)];
13046
+ }
13047
+ return [];
13048
+ }
13049
+ /**
13050
+ * Parse a Bedrock Agent trace event into TrajectorySteps
13051
+ */
13052
+ parseTraceEvent(trace3) {
13053
+ const steps = [];
13054
+ if (trace3.preProcessingTrace) {
13055
+ const pre = trace3.preProcessingTrace;
13056
+ if (pre.modelInvocationOutput?.parsedResponse?.isValid !== void 0) {
13057
+ steps.push(this.createStep(
13058
+ "thinking",
13059
+ `Pre-processing: Input ${pre.modelInvocationOutput.parsedResponse.isValid ? "valid" : "invalid"}` + (pre.modelInvocationOutput.parsedResponse.rationale ? ` \u2014 ${pre.modelInvocationOutput.parsedResponse.rationale}` : "")
13060
+ ));
13061
+ }
13062
+ }
13063
+ if (trace3.orchestrationTrace) {
13064
+ const orch = trace3.orchestrationTrace;
13065
+ if (orch.rationale?.text) {
13066
+ steps.push(this.createStep("thinking", orch.rationale.text));
13067
+ }
13068
+ if (orch.modelInvocationInput?.text) {
13069
+ steps.push(this.createStep("thinking", `Model input: ${this.truncate(orch.modelInvocationInput.text, 500)}`));
13070
+ }
13071
+ if (orch.invocationInput) {
13072
+ const inv = orch.invocationInput;
13073
+ if (inv.actionGroupInvocationInput) {
13074
+ const action = inv.actionGroupInvocationInput;
13075
+ steps.push(this.createStep("action", `Calling ${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`, {
13076
+ toolName: `${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`,
13077
+ toolArgs: action.parameters ? Object.fromEntries(
13078
+ action.parameters.map((p) => [p.name, p.value])
13079
+ ) : void 0
13080
+ }));
13081
+ }
13082
+ if (inv.knowledgeBaseLookupInput) {
13083
+ steps.push(this.createStep("action", `Knowledge base lookup: ${inv.knowledgeBaseLookupInput.text}`, {
13084
+ toolName: "knowledge_base_lookup",
13085
+ toolArgs: { query: inv.knowledgeBaseLookupInput.text }
13086
+ }));
13087
+ }
13088
+ }
13089
+ if (orch.observation) {
13090
+ const obs = orch.observation;
13091
+ if (obs.actionGroupInvocationOutput?.text) {
13092
+ steps.push(this.createStep("tool_result", obs.actionGroupInvocationOutput.text, {
13093
+ status: "SUCCESS"
13094
+ }));
13095
+ }
13096
+ if (obs.knowledgeBaseLookupOutput?.retrievedReferences) {
13097
+ const refs = obs.knowledgeBaseLookupOutput.retrievedReferences;
13098
+ steps.push(this.createStep(
13099
+ "tool_result",
13100
+ `Retrieved ${refs.length} reference(s) from knowledge base`,
13101
+ {
13102
+ status: "SUCCESS",
13103
+ toolOutput: refs.map((r) => ({
13104
+ content: this.truncate(r.content?.text, 200),
13105
+ location: r.location
13106
+ }))
13107
+ }
13108
+ ));
13109
+ }
13110
+ if (obs.finalResponse?.text) {
13111
+ steps.push(this.createStep("response", obs.finalResponse.text));
13112
+ }
13113
+ }
13114
+ }
13115
+ if (trace3.postProcessingTrace) {
13116
+ const post = trace3.postProcessingTrace;
13117
+ if (post.modelInvocationOutput?.parsedResponse?.text) {
13118
+ steps.push(this.createStep(
13119
+ "thinking",
13120
+ `Post-processing: ${this.truncate(post.modelInvocationOutput.parsedResponse.text, 300)}`
13121
+ ));
13122
+ }
13123
+ }
13124
+ if (trace3.failureTrace) {
13125
+ steps.push(this.createStep(
13126
+ "response",
13127
+ `Agent error: ${trace3.failureTrace.failureReason || "Unknown failure"}`
13128
+ ));
13129
+ }
13130
+ return steps;
13131
+ }
13132
+ /**
13133
+ * Health check: verify the agent exists via GetAgent
13134
+ */
13135
+ async healthCheck(endpoint, auth) {
13136
+ try {
13137
+ const config2 = auth;
13138
+ const region = config2.awsRegion || process.env.AWS_REGION || "us-east-1";
13139
+ const { BedrockAgentClient, GetAgentCommand } = await import("@aws-sdk/client-bedrock-agent");
13140
+ const clientConfig = { region };
13141
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
13142
+ clientConfig.credentials = {
13143
+ accessKeyId: auth.awsAccessKeyId,
13144
+ secretAccessKey: auth.awsSecretAccessKey,
13145
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
13146
+ };
13147
+ }
13148
+ const client = new BedrockAgentClient(clientConfig);
13149
+ const result = await client.send(new GetAgentCommand({ agentId: endpoint }));
13150
+ return result.agent?.agentStatus === "PREPARED";
13151
+ } catch (error) {
13152
+ this.error("Health check failed:", error);
13153
+ return false;
13154
+ }
13155
+ }
13156
+ truncate(text, maxLen) {
13157
+ if (!text) return "";
13158
+ return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
13159
+ }
13160
+ };
13161
+ var strandsConnector = new StrandsConnector();
13162
+
12565
13163
  // services/connectors/server.ts
12566
13164
  connectorRegistry.register(subprocessConnector);
12567
13165
  connectorRegistry.register(claudeCodeConnector);
13166
+ connectorRegistry.register(strandsConnector);
12568
13167
  console.log("[Connectors] Server connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
12569
13168
 
12570
13169
  // services/benchmarkRunner.ts
@@ -12670,6 +13269,9 @@ var runStorage = {
12670
13269
  const params = new URLSearchParams();
12671
13270
  if (options.size) params.append("size", options.size.toString());
12672
13271
  if (options.from) params.append("from", options.from.toString());
13272
+ if (options._source && options._source.length > 0) {
13273
+ params.append("fields", options._source.join(","));
13274
+ }
12673
13275
  const query = params.toString() ? `?${params.toString()}` : "";
12674
13276
  return request("GET", `/runs${query}`);
12675
13277
  },
@@ -13316,7 +13918,489 @@ var TracePollingManager = class {
13316
13918
  };
13317
13919
  var tracePollingManager = new TracePollingManager();
13318
13920
 
13921
+ // lib/telemetry/provider.ts
13922
+ import { trace } from "@opentelemetry/api";
13923
+ import { NodeTracerProvider, BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
13924
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
13925
+ import { resourceFromAttributes } from "@opentelemetry/resources";
13926
+
13927
+ // lib/telemetry/constants.ts
13928
+ import {
13929
+ ATTR_TEST_SUITE_NAME,
13930
+ ATTR_TEST_SUITE_RUN_STATUS,
13931
+ ATTR_TEST_CASE_NAME,
13932
+ ATTR_TEST_CASE_RESULT_STATUS,
13933
+ TEST_SUITE_RUN_STATUS_VALUE_SUCCESS,
13934
+ TEST_SUITE_RUN_STATUS_VALUE_FAILURE,
13935
+ TEST_SUITE_RUN_STATUS_VALUE_IN_PROGRESS,
13936
+ TEST_CASE_RESULT_STATUS_VALUE_PASS,
13937
+ TEST_CASE_RESULT_STATUS_VALUE_FAIL,
13938
+ ATTR_GEN_AI_OPERATION_NAME as ATTR_GEN_AI_OPERATION_NAME4,
13939
+ ATTR_GEN_AI_EVALUATION_NAME,
13940
+ ATTR_GEN_AI_EVALUATION_SCORE_VALUE,
13941
+ ATTR_GEN_AI_EVALUATION_SCORE_LABEL,
13942
+ ATTR_GEN_AI_EVALUATION_EXPLANATION,
13943
+ EVENT_GEN_AI_EVALUATION_RESULT
13944
+ } from "@opentelemetry/semantic-conventions/incubating";
13945
+ var ATTR_TEST_SUITE_RUN_ID = "test.suite.run.id";
13946
+ var ATTR_TEST_CASE_ID = "test.case.id";
13947
+ var ATTR_TEST_CASE_INPUT = "test.case.input";
13948
+ var ATTR_TEST_CASE_OUTPUT = "test.case.output";
13949
+ var ATTR_TEST_CASE_EXPECTED = "test.case.expected";
13950
+ var ATTR_AGENT_HEALTH_JUDGE_MODEL_ID = "agent_health.judge.model_id";
13951
+ var ATTR_AGENT_HEALTH_JUDGE_DURATION_MS = "agent_health.judge.duration_ms";
13952
+ var ATTR_AGENT_HEALTH_JUDGE_ATTEMPTS = "agent_health.judge.attempts";
13953
+ var ATTR_AGENT_HEALTH_AGENT_DURATION_MS = "agent_health.agent.duration_ms";
13954
+ var ATTR_AGENT_HEALTH_CONNECTOR_PROTOCOL = "agent_health.connector.protocol";
13955
+ var ATTR_AGENT_HEALTH_AGENT_RUN_ID = "gen_ai.request.id";
13956
+ var GEN_AI_OPERATION_NAME_VALUE_EVALUATION = "evaluation";
13957
+ var MAX_ATTRIBUTE_LENGTH = 1e4;
13958
+ var MAX_EXPLANATION_LENGTH = 4e3;
13959
+ var EVAL_TRACER_NAME = "agent-health-eval";
13960
+
13961
+ // lib/telemetry/opensearchExporter.ts
13962
+ import { ExportResultCode } from "@opentelemetry/core";
13963
+ import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
13964
+ import { Client as Client2 } from "@opensearch-project/opensearch";
13965
+ function hrTimeToDate(hrTime) {
13966
+ const ms = hrTime[0] * 1e3 + hrTime[1] / 1e6;
13967
+ return new Date(ms).toISOString();
13968
+ }
13969
+ function hrTimeToNanos(hrTime) {
13970
+ return hrTime[0] * 1e9 + hrTime[1];
13971
+ }
13972
+ function durationNanos(start, end) {
13973
+ return hrTimeToNanos(end) - hrTimeToNanos(start);
13974
+ }
13975
+ function flattenKey(key) {
13976
+ return key.replace(/\./g, "@");
13977
+ }
13978
+ function spanKindToString(kind) {
13979
+ switch (kind) {
13980
+ case SpanKind.INTERNAL:
13981
+ return "SPAN_KIND_INTERNAL";
13982
+ case SpanKind.SERVER:
13983
+ return "SPAN_KIND_SERVER";
13984
+ case SpanKind.CLIENT:
13985
+ return "SPAN_KIND_CLIENT";
13986
+ case SpanKind.PRODUCER:
13987
+ return "SPAN_KIND_PRODUCER";
13988
+ case SpanKind.CONSUMER:
13989
+ return "SPAN_KIND_CONSUMER";
13990
+ default:
13991
+ return "SPAN_KIND_INTERNAL";
13992
+ }
13993
+ }
13994
+ function statusCodeToInt(code) {
13995
+ switch (code) {
13996
+ case SpanStatusCode.UNSET:
13997
+ return 0;
13998
+ case SpanStatusCode.OK:
13999
+ return 1;
14000
+ case SpanStatusCode.ERROR:
14001
+ return 2;
14002
+ default:
14003
+ return 0;
14004
+ }
14005
+ }
14006
+ function spanToDocument(span) {
14007
+ const doc = {
14008
+ traceId: span.spanContext().traceId,
14009
+ spanId: span.spanContext().spanId,
14010
+ parentSpanId: span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== "0000000000000000" ? span.parentSpanContext.spanId : "",
14011
+ name: span.name,
14012
+ kind: spanKindToString(span.kind),
14013
+ startTime: hrTimeToDate(span.startTime),
14014
+ endTime: hrTimeToDate(span.endTime),
14015
+ durationInNanos: durationNanos(span.startTime, span.endTime),
14016
+ "status.code": statusCodeToInt(span.status.code),
14017
+ "status.message": span.status.message || "",
14018
+ traceState: "",
14019
+ droppedAttributesCount: span.droppedAttributesCount,
14020
+ droppedEventsCount: span.droppedEventsCount,
14021
+ droppedLinksCount: span.droppedLinksCount
14022
+ };
14023
+ for (const [key, value] of Object.entries(span.attributes)) {
14024
+ if (value !== void 0) {
14025
+ doc[`span.attributes.${flattenKey(key)}`] = value;
14026
+ }
14027
+ }
14028
+ const resource = span.resource;
14029
+ if (resource?.attributes) {
14030
+ for (const [key, value] of Object.entries(resource.attributes)) {
14031
+ if (value !== void 0) {
14032
+ doc[`resource.attributes.${flattenKey(key)}`] = value;
14033
+ }
14034
+ }
14035
+ }
14036
+ const serviceName = resource?.attributes?.["service.name"] || "unknown";
14037
+ doc.serviceName = serviceName;
14038
+ const scope = span.instrumentationScope ?? span.instrumentationLibrary ?? {};
14039
+ doc["instrumentationScope.name"] = scope.name || "";
14040
+ if (scope.version) {
14041
+ doc["instrumentationScope.version"] = scope.version;
14042
+ }
14043
+ if (span.events.length > 0) {
14044
+ doc.events = span.events.map((event) => {
14045
+ const eventDoc = {
14046
+ name: event.name,
14047
+ time: hrTimeToDate(event.time),
14048
+ droppedAttributesCount: event.droppedAttributesCount || 0
14049
+ };
14050
+ if (event.attributes) {
14051
+ const attrs = {};
14052
+ for (const [key, value] of Object.entries(event.attributes)) {
14053
+ if (value !== void 0) {
14054
+ attrs[flattenKey(key)] = value;
14055
+ }
14056
+ }
14057
+ eventDoc.attributes = attrs;
14058
+ }
14059
+ return eventDoc;
14060
+ });
14061
+ } else {
14062
+ doc.events = [];
14063
+ }
14064
+ if (span.links.length > 0) {
14065
+ doc.links = span.links.map((link) => ({
14066
+ traceId: link.context.traceId,
14067
+ spanId: link.context.spanId,
14068
+ traceState: link.context.traceState?.serialize() || "",
14069
+ attributes: link.attributes || {},
14070
+ droppedAttributesCount: link.droppedAttributesCount || 0
14071
+ }));
14072
+ } else {
14073
+ doc.links = [];
14074
+ }
14075
+ const hasRealParent = span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== "0000000000000000";
14076
+ if (!hasRealParent) {
14077
+ doc.traceGroup = span.name;
14078
+ doc.traceGroupFields = {
14079
+ endTime: hrTimeToDate(span.endTime),
14080
+ durationInNanos: durationNanos(span.startTime, span.endTime),
14081
+ statusCode: statusCodeToInt(span.status.code)
14082
+ };
14083
+ }
14084
+ return doc;
14085
+ }
14086
+ var OpenSearchSpanExporter = class {
14087
+ constructor(config2) {
14088
+ this.shutdownRequested = false;
14089
+ const auth = config2.username && config2.password ? `${config2.username}:${config2.password}` : void 0;
14090
+ this.client = new Client2({
14091
+ node: config2.endpoint,
14092
+ auth: auth ? { username: config2.username, password: config2.password } : void 0,
14093
+ ssl: {
14094
+ rejectUnauthorized: config2.tlsSkipVerify !== true
14095
+ }
14096
+ });
14097
+ this.indexName = config2.indexName || "otel-v1-apm-span-000001";
14098
+ console.log(`[Telemetry] OpenSearch exporter initialized \u2192 ${config2.endpoint} (index: ${this.indexName})`);
14099
+ }
14100
+ export(spans, resultCallback) {
14101
+ if (this.shutdownRequested) {
14102
+ resultCallback({ code: ExportResultCode.FAILED });
14103
+ return;
14104
+ }
14105
+ this._export(spans).then(() => resultCallback({ code: ExportResultCode.SUCCESS })).catch((err) => {
14106
+ console.error("[Telemetry] OpenSearch export failed:", err instanceof Error ? err.message : err);
14107
+ resultCallback({ code: ExportResultCode.FAILED });
14108
+ });
14109
+ }
14110
+ async _export(spans) {
14111
+ if (spans.length === 0) return;
14112
+ const body = [];
14113
+ for (const span of spans) {
14114
+ const doc = spanToDocument(span);
14115
+ const docId = `${doc.traceId}/${doc.spanId}`;
14116
+ body.push({ index: { _index: this.indexName, _id: docId } });
14117
+ body.push(doc);
14118
+ }
14119
+ const response = await this.client.bulk({ body });
14120
+ if (response.body.errors) {
14121
+ const errors = response.body.items.filter((item) => item.index?.error).map((item) => `${item.index.error.type}: ${item.index.error.reason}`);
14122
+ if (errors.length > 0) {
14123
+ console.warn(`[Telemetry] OpenSearch bulk write had ${errors.length} error(s):`, errors[0]);
14124
+ }
14125
+ } else {
14126
+ console.log(`[Telemetry] Exported ${spans.length} span(s) to OpenSearch`);
14127
+ }
14128
+ }
14129
+ async shutdown() {
14130
+ this.shutdownRequested = true;
14131
+ await this.client.close();
14132
+ }
14133
+ async forceFlush() {
14134
+ }
14135
+ };
14136
+
14137
+ // lib/telemetry/provider.ts
14138
+ var provider = null;
14139
+ var telemetryEnabled = false;
14140
+ function resolveEvalTelemetryConfig(userConfig) {
14141
+ const enabled = userConfig?.enabled ?? process.env.OTEL_EVAL_ENABLED === "true";
14142
+ const exporterEndpoint = userConfig?.exporterEndpoint ?? process.env.OTEL_EVAL_EXPORTER_ENDPOINT ?? "http://localhost:4318/v1/traces";
14143
+ let exporterHeaders = userConfig?.exporterHeaders;
14144
+ if (!exporterHeaders && process.env.OTEL_EVAL_EXPORTER_HEADERS) {
14145
+ try {
14146
+ exporterHeaders = JSON.parse(process.env.OTEL_EVAL_EXPORTER_HEADERS);
14147
+ } catch {
14148
+ console.warn("[Telemetry] Failed to parse OTEL_EVAL_EXPORTER_HEADERS as JSON");
14149
+ }
14150
+ }
14151
+ const serviceName = userConfig?.serviceName ?? process.env.OTEL_EVAL_SERVICE_NAME ?? "agent-health";
14152
+ return { enabled, exporterEndpoint, exporterHeaders, serviceName, opensearch: userConfig?.opensearch };
14153
+ }
14154
+ function initEvalTracerProvider(config2) {
14155
+ if (provider) {
14156
+ return;
14157
+ }
14158
+ if (!config2.enabled) {
14159
+ telemetryEnabled = false;
14160
+ console.log("[Telemetry] Evaluation telemetry disabled (OTEL_EVAL_ENABLED != true)");
14161
+ return;
14162
+ }
14163
+ const resource = resourceFromAttributes({
14164
+ "service.name": config2.serviceName ?? "agent-health",
14165
+ "telemetry.sdk.name": "agent-health",
14166
+ "telemetry.sdk.language": "nodejs"
14167
+ });
14168
+ const spanProcessors = [];
14169
+ const hasExplicitOtlpEndpoint = config2.exporterEndpoint !== "http://localhost:4318/v1/traces";
14170
+ if (hasExplicitOtlpEndpoint) {
14171
+ const otlpExporter = new OTLPTraceExporter({
14172
+ url: config2.exporterEndpoint,
14173
+ headers: config2.exporterHeaders
14174
+ });
14175
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter));
14176
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OTLP (${config2.exporterEndpoint})`);
14177
+ } else if (config2.opensearch) {
14178
+ const osExporter = new OpenSearchSpanExporter(config2.opensearch);
14179
+ spanProcessors.push(new SimpleSpanProcessor(osExporter));
14180
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OpenSearch direct (${config2.opensearch.endpoint})`);
14181
+ } else {
14182
+ const otlpExporter = new OTLPTraceExporter({
14183
+ url: config2.exporterEndpoint,
14184
+ headers: config2.exporterHeaders
14185
+ });
14186
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter));
14187
+ console.log(`[Telemetry] Evaluation telemetry enabled \u2192 OTLP (${config2.exporterEndpoint})`);
14188
+ }
14189
+ provider = new NodeTracerProvider({
14190
+ resource,
14191
+ spanProcessors
14192
+ });
14193
+ provider.register();
14194
+ telemetryEnabled = true;
14195
+ }
14196
+ function getEvalTracer() {
14197
+ return trace.getTracer(EVAL_TRACER_NAME);
14198
+ }
14199
+ function isEvalTelemetryEnabled() {
14200
+ return telemetryEnabled;
14201
+ }
14202
+ async function flushEvalTracer() {
14203
+ if (provider) {
14204
+ try {
14205
+ await provider.forceFlush();
14206
+ } catch (err) {
14207
+ console.warn("[Telemetry] forceFlush failed:", err instanceof Error ? err.message : err);
14208
+ }
14209
+ }
14210
+ }
14211
+ async function shutdownEvalTracer() {
14212
+ if (provider) {
14213
+ await provider.shutdown();
14214
+ provider = null;
14215
+ telemetryEnabled = false;
14216
+ console.log("[Telemetry] Evaluation telemetry shut down");
14217
+ }
14218
+ }
14219
+
14220
+ // lib/telemetry/evalSpans.ts
14221
+ import {
14222
+ context,
14223
+ trace as trace2,
14224
+ TraceFlags,
14225
+ SpanKind as SpanKind2,
14226
+ SpanStatusCode as SpanStatusCode2
14227
+ } from "@opentelemetry/api";
14228
+ function truncate(value, maxLength) {
14229
+ if (!value) return void 0;
14230
+ return value.length > maxLength ? value.slice(0, maxLength) + "..." : value;
14231
+ }
14232
+ function extractAgentResponse(report) {
14233
+ const responseStep = [...report.trajectory].reverse().find(
14234
+ (s) => s.type === "response" || s.type === "assistant"
14235
+ );
14236
+ return responseStep?.content;
14237
+ }
14238
+ function startTestSuiteRunSpan(benchmark, run) {
14239
+ if (!isEvalTelemetryEnabled()) return null;
14240
+ const tracer = getEvalTracer();
14241
+ const span = tracer.startSpan(`test_suite_run ${benchmark.name}`, {
14242
+ kind: SpanKind2.INTERNAL,
14243
+ attributes: {
14244
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14245
+ [ATTR_TEST_SUITE_RUN_ID]: run.id,
14246
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14247
+ }
14248
+ });
14249
+ const ctx = context.active();
14250
+ const spanContext = trace2.setSpan(ctx, span);
14251
+ console.log(`[Telemetry] Started test_suite_run span for "${benchmark.name}" (runId=${run.id})`);
14252
+ return { span, context: spanContext };
14253
+ }
14254
+ function startTestCaseSpan(parentCtx, testCase, benchmark, run, agentRunId, links) {
14255
+ if (!isEvalTelemetryEnabled()) return null;
14256
+ const tracer = getEvalTracer();
14257
+ const attributes = {
14258
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14259
+ [ATTR_TEST_SUITE_RUN_ID]: run.id,
14260
+ [ATTR_TEST_CASE_ID]: testCase.id,
14261
+ [ATTR_TEST_CASE_NAME]: testCase.name,
14262
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14263
+ };
14264
+ if (agentRunId) {
14265
+ attributes[ATTR_AGENT_HEALTH_AGENT_RUN_ID] = agentRunId;
14266
+ }
14267
+ attributes[ATTR_TEST_CASE_INPUT] = truncate(testCase.initialPrompt || "", MAX_ATTRIBUTE_LENGTH);
14268
+ const expected = truncate(
14269
+ testCase.expectedOutcomes ? JSON.stringify(testCase.expectedOutcomes) : void 0,
14270
+ MAX_ATTRIBUTE_LENGTH
14271
+ );
14272
+ if (expected) attributes[ATTR_TEST_CASE_EXPECTED] = expected;
14273
+ const span = tracer.startSpan(
14274
+ "test_case",
14275
+ {
14276
+ kind: SpanKind2.INTERNAL,
14277
+ attributes,
14278
+ links
14279
+ },
14280
+ parentCtx
14281
+ );
14282
+ const ctx = trace2.setSpan(parentCtx, span);
14283
+ console.log(`[Telemetry] Started test_case span for "${testCase.name}" (agentRunId=${agentRunId || "none"})`);
14284
+ return { span, context: ctx };
14285
+ }
14286
+ function addEvaluationResultEvents(span, report) {
14287
+ if (!isEvalTelemetryEnabled()) return;
14288
+ if (report.metrics?.accuracy !== void 0) {
14289
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14290
+ [ATTR_GEN_AI_EVALUATION_NAME]: "accuracy",
14291
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.accuracy,
14292
+ [ATTR_GEN_AI_EVALUATION_SCORE_LABEL]: report.passFailStatus === "passed" ? "pass" : "fail",
14293
+ [ATTR_GEN_AI_EVALUATION_EXPLANATION]: truncate(report.llmJudgeReasoning, MAX_EXPLANATION_LENGTH)
14294
+ });
14295
+ }
14296
+ if (report.metrics?.faithfulness !== void 0) {
14297
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14298
+ [ATTR_GEN_AI_EVALUATION_NAME]: "faithfulness",
14299
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.faithfulness
14300
+ });
14301
+ }
14302
+ if (report.metrics?.latency_score !== void 0) {
14303
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14304
+ [ATTR_GEN_AI_EVALUATION_NAME]: "latency_score",
14305
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.latency_score
14306
+ });
14307
+ }
14308
+ if (report.metrics?.trajectory_alignment_score !== void 0) {
14309
+ span.addEvent(EVENT_GEN_AI_EVALUATION_RESULT, {
14310
+ [ATTR_GEN_AI_EVALUATION_NAME]: "trajectory_alignment_score",
14311
+ [ATTR_GEN_AI_EVALUATION_SCORE_VALUE]: report.metrics.trajectory_alignment_score
14312
+ });
14313
+ }
14314
+ }
14315
+ function finalizeTestCaseSpan(span, report, endTime) {
14316
+ if (!isEvalTelemetryEnabled()) return;
14317
+ const status = report.passFailStatus === "passed" ? TEST_CASE_RESULT_STATUS_VALUE_PASS : TEST_CASE_RESULT_STATUS_VALUE_FAIL;
14318
+ span.setAttribute(ATTR_TEST_CASE_RESULT_STATUS, status);
14319
+ const output = extractAgentResponse(report) || "";
14320
+ span.setAttribute(ATTR_TEST_CASE_OUTPUT, truncate(output, MAX_ATTRIBUTE_LENGTH));
14321
+ if (report.connectorProtocol) {
14322
+ span.setAttribute(ATTR_AGENT_HEALTH_CONNECTOR_PROTOCOL, report.connectorProtocol);
14323
+ }
14324
+ if (report.performanceMetrics?.agentDurationMs !== void 0) {
14325
+ span.setAttribute(ATTR_AGENT_HEALTH_AGENT_DURATION_MS, report.performanceMetrics.agentDurationMs);
14326
+ }
14327
+ if (report.performanceMetrics?.judgeDurationMs !== void 0) {
14328
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_DURATION_MS, report.performanceMetrics.judgeDurationMs);
14329
+ }
14330
+ if (report.performanceMetrics?.judgeAttempts !== void 0) {
14331
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_ATTEMPTS, report.performanceMetrics.judgeAttempts);
14332
+ }
14333
+ if (report.llmJudgeResponse?.modelId) {
14334
+ span.setAttribute(ATTR_AGENT_HEALTH_JUDGE_MODEL_ID, report.llmJudgeResponse.modelId);
14335
+ }
14336
+ if (report.status === "failed") {
14337
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: "Evaluation failed" });
14338
+ }
14339
+ span.end(endTime);
14340
+ console.log(`[Telemetry] Finalized test_case span (status=${status}, passFailStatus=${report.passFailStatus})`);
14341
+ }
14342
+ function finalizeTestSuiteRunSpan(span, run) {
14343
+ if (!isEvalTelemetryEnabled()) return;
14344
+ const results = Object.values(run.results);
14345
+ const hasFailures = results.some((r) => r.status === "failed");
14346
+ const status = hasFailures ? TEST_SUITE_RUN_STATUS_VALUE_FAILURE : TEST_SUITE_RUN_STATUS_VALUE_SUCCESS;
14347
+ span.setAttribute(ATTR_TEST_SUITE_RUN_STATUS, status);
14348
+ if (hasFailures) {
14349
+ const failCount = results.filter((r) => r.status === "failed").length;
14350
+ span.setStatus({ code: SpanStatusCode2.ERROR, message: `${failCount} case(s) failed` });
14351
+ }
14352
+ span.end();
14353
+ console.log(`[Telemetry] Finalized test_suite_run span (status=${status}), flushing...`);
14354
+ flushEvalTracer().catch(() => {
14355
+ });
14356
+ }
14357
+ function emitDeferredTestCaseSpan(testCase, report, benchmark, runId, agentRunId, startTime, endTime, agentTraceId) {
14358
+ if (!isEvalTelemetryEnabled()) return;
14359
+ const tracer = getEvalTracer();
14360
+ const attributes = {
14361
+ [ATTR_TEST_SUITE_NAME]: benchmark.name,
14362
+ [ATTR_TEST_SUITE_RUN_ID]: runId,
14363
+ [ATTR_TEST_CASE_ID]: testCase.id,
14364
+ [ATTR_TEST_CASE_NAME]: testCase.name,
14365
+ [ATTR_GEN_AI_OPERATION_NAME4]: GEN_AI_OPERATION_NAME_VALUE_EVALUATION
14366
+ };
14367
+ if (agentRunId) {
14368
+ attributes[ATTR_AGENT_HEALTH_AGENT_RUN_ID] = agentRunId;
14369
+ }
14370
+ attributes[ATTR_TEST_CASE_INPUT] = truncate(testCase.initialPrompt || "", MAX_ATTRIBUTE_LENGTH);
14371
+ const expected = truncate(
14372
+ testCase.expectedOutcomes ? JSON.stringify(testCase.expectedOutcomes) : void 0,
14373
+ MAX_ATTRIBUTE_LENGTH
14374
+ );
14375
+ if (expected) attributes[ATTR_TEST_CASE_EXPECTED] = expected;
14376
+ let parentCtx;
14377
+ if (agentTraceId) {
14378
+ const remoteSpanContext = {
14379
+ traceId: agentTraceId,
14380
+ // Use a valid synthetic spanId — an all-zero spanId is considered invalid
14381
+ // by the OTel SDK and would cause it to ignore this context entirely.
14382
+ // This "remote" context carries the traceId forward so the eval span
14383
+ // shares the same trace as the agent spans.
14384
+ spanId: "eeee000000000001",
14385
+ traceFlags: TraceFlags.SAMPLED,
14386
+ isRemote: true
14387
+ };
14388
+ parentCtx = trace2.setSpanContext(context.active(), remoteSpanContext);
14389
+ }
14390
+ const span = tracer.startSpan("test_case", {
14391
+ kind: SpanKind2.INTERNAL,
14392
+ startTime,
14393
+ attributes
14394
+ }, parentCtx);
14395
+ addEvaluationResultEvents(span, report);
14396
+ finalizeTestCaseSpan(span, report, endTime);
14397
+ console.log(`[Telemetry] Emitted deferred test_case span for "${testCase.name}" (agentRunId=${agentRunId || "none"}, traceId=${agentTraceId || "new"}), flushing...`);
14398
+ flushEvalTracer().catch(() => {
14399
+ });
14400
+ }
14401
+
13319
14402
  // services/benchmarkRunner.ts
14403
+ import { SpanStatusCode as SpanStatusCode3 } from "@opentelemetry/api";
13320
14404
  function getConfig() {
13321
14405
  try {
13322
14406
  return loadConfigSync();
@@ -13374,6 +14458,9 @@ async function executeRun(benchmark, run, onProgress, options) {
13374
14458
  const concurrency = run.concurrency ?? 1;
13375
14459
  const runStartTime = Date.now();
13376
14460
  console.log(`[BenchmarkRunner] Starting run ${run.id} with concurrency=${concurrency} for ${totalTestCases} test cases`);
14461
+ const suiteSpanResult = startTestSuiteRunSpan(benchmark, run);
14462
+ const suiteSpan = suiteSpanResult?.span;
14463
+ const suiteContext = suiteSpanResult?.context;
13377
14464
  if (!run.results) {
13378
14465
  run.results = {};
13379
14466
  }
@@ -13424,6 +14511,7 @@ async function executeRun(benchmark, run, onProgress, options) {
13424
14511
  });
13425
14512
  debug("BenchmarkRunner", `[${testCaseId}] Starting evaluation (${completedCount}/${totalTestCases} completed)`);
13426
14513
  const testCaseStartTime = Date.now();
14514
+ let caseSpan;
13427
14515
  run.results[testCaseId] = { reportId: "", status: "running" };
13428
14516
  try {
13429
14517
  const agentConfig = buildAgentConfigForRun(run);
@@ -13443,7 +14531,23 @@ async function executeRun(benchmark, run, onProgress, options) {
13443
14531
  });
13444
14532
  updateTestCaseLastRunAt(client, testCaseId, (/* @__PURE__ */ new Date()).toISOString()).catch((err) => console.warn(`[BenchmarkRunner] Failed to update lastRunAt for ${testCaseId}:`, err.message));
13445
14533
  if (savedReport.metricsStatus === "pending" && savedReport.runId) {
13446
- startTracePollingForReport(savedReport, testCase, client);
14534
+ startTracePollingForReport(savedReport, testCase, client, benchmark, run);
14535
+ }
14536
+ if (suiteContext) {
14537
+ const caseSpanResult = startTestCaseSpan(
14538
+ suiteContext,
14539
+ testCase,
14540
+ benchmark,
14541
+ run,
14542
+ savedReport.runId
14543
+ );
14544
+ caseSpan = caseSpanResult?.span;
14545
+ if (caseSpan && savedReport.metricsStatus !== "pending") {
14546
+ addEvaluationResultEvents(caseSpan, savedReport);
14547
+ finalizeTestCaseSpan(caseSpan, savedReport);
14548
+ } else if (caseSpan) {
14549
+ caseSpan.end();
14550
+ }
13447
14551
  }
13448
14552
  run.results[testCaseId] = {
13449
14553
  reportId: savedReport.id,
@@ -13462,6 +14566,10 @@ async function executeRun(benchmark, run, onProgress, options) {
13462
14566
  const testCaseDuration = Date.now() - testCaseStartTime;
13463
14567
  debug("BenchmarkRunner", `[${testCaseId}] Failed in ${testCaseDuration}ms: ${errorMsg}`);
13464
14568
  run.results[testCaseId] = { reportId: "", status: "failed", error: errorMsg };
14569
+ if (caseSpan) {
14570
+ caseSpan.setStatus({ code: SpanStatusCode3.ERROR, message: errorMsg });
14571
+ caseSpan.end();
14572
+ }
13465
14573
  completedCount++;
13466
14574
  if (errorMsg.includes("ThrottlingException") || errorMsg.includes("rate limit") || errorMsg.includes("429")) {
13467
14575
  consecutiveThrottles++;
@@ -13505,6 +14613,9 @@ async function executeRun(benchmark, run, onProgress, options) {
13505
14613
  maxTestCaseDurationMs: testCaseDurations.length > 0 ? Math.max(...testCaseDurations) : 0,
13506
14614
  minTestCaseDurationMs: testCaseDurations.length > 0 ? Math.min(...testCaseDurations) : 0
13507
14615
  };
14616
+ if (suiteSpan) {
14617
+ finalizeTestSuiteRunSpan(suiteSpan, run);
14618
+ }
13508
14619
  return run;
13509
14620
  } catch (error) {
13510
14621
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -13513,6 +14624,10 @@ async function executeRun(benchmark, run, onProgress, options) {
13513
14624
  run.results[testCaseId] = { reportId: "", status: "failed", error: `Benchmark execution failed: ${errorMsg}` };
13514
14625
  }
13515
14626
  });
14627
+ if (suiteSpan) {
14628
+ suiteSpan.setStatus({ code: SpanStatusCode3.ERROR, message: errorMsg });
14629
+ suiteSpan.end();
14630
+ }
13516
14631
  throw error;
13517
14632
  }
13518
14633
  }
@@ -13544,6 +14659,7 @@ async function saveReportWithModule(storage, report) {
13544
14659
  async function runSingleUseCase(run, testCase, storage, onStep) {
13545
14660
  const agentConfig = buildAgentConfigForRun(run);
13546
14661
  const bedrockModelId = getBedrockModelId(run.modelId);
14662
+ const startTime = /* @__PURE__ */ new Date();
13547
14663
  const report = await runEvaluationWithConnector(
13548
14664
  agentConfig,
13549
14665
  bedrockModelId,
@@ -13561,6 +14677,17 @@ async function runSingleUseCase(run, testCase, storage, onStep) {
13561
14677
  if (savedReport.metricsStatus === "pending" && savedReport.runId) {
13562
14678
  startTracePollingForReportWithModule(savedReport, testCase, storage);
13563
14679
  }
14680
+ if (savedReport.metricsStatus !== "pending") {
14681
+ emitDeferredTestCaseSpan(
14682
+ testCase,
14683
+ savedReport,
14684
+ { name: `standalone:${agentConfig.name || agentConfig.key}` },
14685
+ run.id,
14686
+ savedReport.runId,
14687
+ startTime,
14688
+ /* @__PURE__ */ new Date()
14689
+ );
14690
+ }
13564
14691
  return savedReport.id;
13565
14692
  }
13566
14693
  function startTracePollingForReportWithModule(report, testCase, storage) {
@@ -13600,6 +14727,24 @@ function startTracePollingForReportWithModule(report, testCase, storage) {
13600
14727
  llmJudgeReasoning: judgment.llmJudgeReasoning,
13601
14728
  improvementStrategies: judgment.improvementStrategies
13602
14729
  });
14730
+ const completedReport = {
14731
+ ...report,
14732
+ passFailStatus: judgment.passFailStatus,
14733
+ metrics: judgment.metrics,
14734
+ llmJudgeReasoning: judgment.llmJudgeReasoning
14735
+ };
14736
+ emitDeferredTestCaseSpan(
14737
+ testCase,
14738
+ completedReport,
14739
+ { name: report.experimentId ? `benchmark:${report.experimentId}` : `standalone:${report.agentKey}` },
14740
+ report.experimentRunId || report.id,
14741
+ report.runId,
14742
+ void 0,
14743
+ // startTime
14744
+ void 0,
14745
+ // endTime
14746
+ spans[0]?.traceId
14747
+ );
13603
14748
  if (report.experimentId) {
13604
14749
  await refreshBenchmarkRunStats(storage, report.experimentId, report.id);
13605
14750
  }
@@ -13627,7 +14772,7 @@ function startTracePollingForReportWithModule(report, testCase, storage) {
13627
14772
  }
13628
14773
  );
13629
14774
  }
13630
- function startTracePollingForReport(report, testCase, client) {
14775
+ function startTracePollingForReport(report, testCase, client, benchmark, run) {
13631
14776
  if (!report.runId) {
13632
14777
  console.warn(`[BenchmarkRunner] No runId for report ${report.id}, cannot start trace polling`);
13633
14778
  return;
@@ -13662,6 +14807,16 @@ function startTracePollingForReport(report, testCase, client) {
13662
14807
  if (report.experimentId) {
13663
14808
  await updateBenchmarkRunStatsForReport(client, report.experimentId, report.id);
13664
14809
  }
14810
+ if (benchmark && run) {
14811
+ const completedReport = {
14812
+ ...report,
14813
+ passFailStatus: judgment.passFailStatus,
14814
+ metrics: judgment.metrics,
14815
+ llmJudgeReasoning: judgment.llmJudgeReasoning
14816
+ };
14817
+ const agentTraceId = spans[0]?.traceId;
14818
+ emitDeferredTestCaseSpan(testCase, completedReport, benchmark, run.id, report.runId, void 0, void 0, agentTraceId);
14819
+ }
13665
14820
  } catch (error) {
13666
14821
  console.error(`[BenchmarkRunner] Failed to judge report ${report.id}:`, error instanceof Error ? error.message : error);
13667
14822
  await updateRunWithClient(client, report.id, {
@@ -15097,11 +16252,13 @@ router10.get("/api/storage/runs", async (req, res) => {
15097
16252
  try {
15098
16253
  const { size = "100", from = "0", fields } = req.query;
15099
16254
  let realData = [];
16255
+ const fieldList = typeof fields === "string" ? fields.split(",").map((f) => f.trim()).filter(Boolean) : void 0;
15100
16256
  const storage = getStorageModule();
15101
16257
  try {
15102
16258
  const result = await storage.runs.getAll({
15103
16259
  size: parseInt(size),
15104
- from: parseInt(from)
16260
+ from: parseInt(from),
16261
+ _source: fieldList
15105
16262
  });
15106
16263
  realData = result.items;
15107
16264
  } catch (e) {
@@ -19263,7 +20420,25 @@ async function initializeStorageBackend() {
19263
20420
  }
19264
20421
  async function createApp() {
19265
20422
  await migrateYamlToJsonIfNeeded();
19266
- await loadConfig();
20423
+ const config2 = await loadConfig();
20424
+ const obsConfig = getObservabilityConfigFromFile() ?? getObservabilityConfigFromEnv();
20425
+ let opensearchExporterConfig;
20426
+ if (obsConfig?.endpoint) {
20427
+ opensearchExporterConfig = {
20428
+ endpoint: obsConfig.endpoint,
20429
+ username: obsConfig.username,
20430
+ password: obsConfig.password,
20431
+ tlsSkipVerify: obsConfig.tlsSkipVerify,
20432
+ indexName: "otel-v1-apm-span"
20433
+ };
20434
+ }
20435
+ initEvalTracerProvider(resolveEvalTelemetryConfig({
20436
+ ...config2.telemetry,
20437
+ opensearch: opensearchExporterConfig
20438
+ }));
20439
+ process.once("SIGTERM", async () => {
20440
+ await shutdownEvalTracer();
20441
+ });
19267
20442
  await initializeStorageBackend();
19268
20443
  const app = express2();
19269
20444
  setupMiddleware(app);