@opensearch-project/agent-health 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -259
- package/cli/dist/index.js +905 -239
- package/dist/assets/index-EvPLSTAS.js +267 -0
- package/dist/assets/index-RXasQKUs.css +1 -0
- package/dist/index.html +2 -2
- package/dist/opensearch-logo-dark.svg +5 -0
- package/dist/opensearch-logo-light.svg +5 -0
- package/dist/test-first-run-improved.html +469 -0
- package/lib/dist/config/index.js +97 -56
- package/lib/dist/index.js +82 -6
- package/package.json +6 -4
- package/server/dist/app.js +12212 -6867
- package/server/dist/index.js +8782 -3329
- package/dist/assets/index-D5yuaEp4.js +0 -267
- package/dist/assets/index-D6wGwYUm.css +0 -1
package/cli/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
|
+
import chalk11 from "chalk";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
|
|
8
8
|
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
@@ -28,6 +28,8 @@ function findPackageRoot() {
|
|
|
28
28
|
}
|
|
29
29
|
async function startServer(options) {
|
|
30
30
|
process.env.VITE_BACKEND_PORT = String(options.port);
|
|
31
|
+
if (options.headless) process.env.AGENT_HEALTH_HEADLESS = "1";
|
|
32
|
+
if (options.apiKey) process.env.AGENT_HEALTH_API_KEY = options.apiKey;
|
|
31
33
|
const packageRoot = findPackageRoot();
|
|
32
34
|
const serverPath = join(packageRoot, "server", "dist", "app.js");
|
|
33
35
|
const { createApp } = await import(serverPath);
|
|
@@ -96,9 +98,9 @@ var ENV_CONFIG = {
|
|
|
96
98
|
mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
|
|
97
99
|
// Travel Planner multi-agent endpoint (OTel Demo in Docker)
|
|
98
100
|
travelPlannerEndpoint: getEnvVar("TRAVEL_PLANNER_ENDPOINT", "http://localhost:3000"),
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
// OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
|
|
102
|
+
openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
|
|
103
|
+
openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
|
|
102
104
|
// Claude Code Telemetry (optional - for OTEL traces from Claude Code)
|
|
103
105
|
claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
|
|
104
106
|
otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
|
|
@@ -106,39 +108,47 @@ var ENV_CONFIG = {
|
|
|
106
108
|
otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
|
|
107
109
|
otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
108
110
|
};
|
|
109
|
-
function buildMLCommonsHeaders() {
|
|
110
|
-
const headers = {};
|
|
111
|
-
if (ENV_CONFIG.mlcommonsHeaderOpenSearchUrl) {
|
|
112
|
-
headers["opensearch-url"] = ENV_CONFIG.mlcommonsHeaderOpenSearchUrl;
|
|
113
|
-
}
|
|
114
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsRegion) {
|
|
115
|
-
headers["aws-region"] = ENV_CONFIG.mlcommonsHeaderAwsRegion;
|
|
116
|
-
}
|
|
117
|
-
if (ENV_CONFIG.mlcommonsHeaderAuthorization) {
|
|
118
|
-
headers["Authorization"] = ENV_CONFIG.mlcommonsHeaderAuthorization;
|
|
119
|
-
} else {
|
|
120
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsServiceName) {
|
|
121
|
-
headers["aws-service-name"] = ENV_CONFIG.mlcommonsHeaderAwsServiceName;
|
|
122
|
-
}
|
|
123
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId) {
|
|
124
|
-
headers["aws-access-key-id"] = ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId;
|
|
125
|
-
}
|
|
126
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey) {
|
|
127
|
-
headers["aws-secret-access-key"] = ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey;
|
|
128
|
-
}
|
|
129
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSessionToken) {
|
|
130
|
-
headers["aws-session-token"] = ENV_CONFIG.mlcommonsHeaderAwsSessionToken;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return headers;
|
|
134
|
-
}
|
|
135
111
|
|
|
136
112
|
// lib/constants.ts
|
|
113
|
+
var CONNECTOR_TYPE_INFO = {
|
|
114
|
+
"agui-streaming": {
|
|
115
|
+
label: "AG-UI Streaming",
|
|
116
|
+
description: "AG-UI protocol over SSE. Use for ML-Commons and AG-UI compatible agents.",
|
|
117
|
+
serverOnly: false
|
|
118
|
+
},
|
|
119
|
+
"rest": {
|
|
120
|
+
label: "REST",
|
|
121
|
+
description: "Standard HTTP POST. Agent receives JSON, returns JSON. No streaming.",
|
|
122
|
+
serverOnly: false
|
|
123
|
+
},
|
|
124
|
+
"openai-compatible": {
|
|
125
|
+
label: "OpenAI Compatible",
|
|
126
|
+
description: "OpenAI chat completions format (POST /v1/chat/completions). Works with LiteLLM, Ollama, vLLM.",
|
|
127
|
+
serverOnly: false
|
|
128
|
+
},
|
|
129
|
+
"subprocess": {
|
|
130
|
+
label: "Subprocess",
|
|
131
|
+
description: "Runs a CLI command as a child process. Server-only \u2014 use the CLI or benchmark runner.",
|
|
132
|
+
serverOnly: true
|
|
133
|
+
},
|
|
134
|
+
"claude-code": {
|
|
135
|
+
label: "Claude Code",
|
|
136
|
+
description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
|
|
137
|
+
serverOnly: true
|
|
138
|
+
},
|
|
139
|
+
"mock": {
|
|
140
|
+
label: "Mock",
|
|
141
|
+
description: "Built-in demo agent for testing. No real endpoint needed.",
|
|
142
|
+
serverOnly: false
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var VALID_CONNECTOR_TYPES = Object.keys(CONNECTOR_TYPE_INFO);
|
|
146
|
+
var BROWSER_SAFE_CONNECTORS = Object.entries(CONNECTOR_TYPE_INFO).filter(([, info]) => !info.serverOnly).map(([type]) => type);
|
|
137
147
|
function getClaudeCodeConnectorEnv() {
|
|
138
148
|
const env = {
|
|
139
|
-
AWS_PROFILE:
|
|
149
|
+
AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
|
|
140
150
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
141
|
-
AWS_REGION:
|
|
151
|
+
AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
|
|
142
152
|
DISABLE_PROMPT_CACHING: "1",
|
|
143
153
|
DISABLE_ERROR_REPORTING: "1"
|
|
144
154
|
};
|
|
@@ -165,37 +175,15 @@ var DEFAULT_CONFIG = {
|
|
|
165
175
|
endpoint: "mock://demo",
|
|
166
176
|
description: "Mock agent for testing (simulated responses)",
|
|
167
177
|
connectorType: "mock",
|
|
168
|
-
models: ["demo-model"],
|
|
169
178
|
headers: {},
|
|
170
179
|
useTraces: false
|
|
171
180
|
},
|
|
172
|
-
{
|
|
173
|
-
key: "mlcommons-local",
|
|
174
|
-
name: "ML-Commons (Localhost)",
|
|
175
|
-
endpoint: ENV_CONFIG.mlcommonsEndpoint,
|
|
176
|
-
description: "Local OpenSearch ML-Commons conversational agent",
|
|
177
|
-
connectorType: "agui-streaming",
|
|
178
|
-
models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
|
|
179
|
-
headers: buildMLCommonsHeaders(),
|
|
180
|
-
useTraces: true
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
key: "travel-planner",
|
|
184
|
-
name: "Travel Planner",
|
|
185
|
-
endpoint: ENV_CONFIG.travelPlannerEndpoint,
|
|
186
|
-
description: "Multi-agent Travel Planner demo (requires OTel Demo running via Docker)",
|
|
187
|
-
connectorType: "agui-streaming",
|
|
188
|
-
models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
|
|
189
|
-
headers: {},
|
|
190
|
-
useTraces: true
|
|
191
|
-
},
|
|
192
181
|
{
|
|
193
182
|
key: "claude-code",
|
|
194
183
|
name: "Claude Code",
|
|
195
184
|
endpoint: "claude",
|
|
196
185
|
description: "Claude Code CLI agent (requires claude command installed)",
|
|
197
186
|
connectorType: "claude-code",
|
|
198
|
-
models: ["claude-sonnet-4"],
|
|
199
187
|
headers: {},
|
|
200
188
|
useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
|
|
201
189
|
connectorConfig: { env: getClaudeCodeConnectorEnv() }
|
|
@@ -209,6 +197,48 @@ var DEFAULT_CONFIG = {
|
|
|
209
197
|
context_window: 2e5,
|
|
210
198
|
max_output_tokens: 4096
|
|
211
199
|
},
|
|
200
|
+
"claude-opus-4.6": {
|
|
201
|
+
model_id: "us.anthropic.claude-opus-4-6-v1",
|
|
202
|
+
display_name: "Claude Opus 4.6",
|
|
203
|
+
provider: "bedrock",
|
|
204
|
+
context_window: 2e5,
|
|
205
|
+
max_output_tokens: 128e3
|
|
206
|
+
},
|
|
207
|
+
"claude-sonnet-4.6": {
|
|
208
|
+
model_id: "us.anthropic.claude-sonnet-4-6",
|
|
209
|
+
display_name: "Claude Sonnet 4.6",
|
|
210
|
+
provider: "bedrock",
|
|
211
|
+
context_window: 2e5,
|
|
212
|
+
max_output_tokens: 64e3
|
|
213
|
+
},
|
|
214
|
+
"claude-haiku-4.5": {
|
|
215
|
+
model_id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
216
|
+
display_name: "Claude Haiku 4.5",
|
|
217
|
+
provider: "bedrock",
|
|
218
|
+
context_window: 2e5,
|
|
219
|
+
max_output_tokens: 64e3
|
|
220
|
+
},
|
|
221
|
+
"claude-opus-4.5": {
|
|
222
|
+
model_id: "us.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
223
|
+
display_name: "Claude Opus 4.5",
|
|
224
|
+
provider: "bedrock",
|
|
225
|
+
context_window: 2e5,
|
|
226
|
+
max_output_tokens: 64e3
|
|
227
|
+
},
|
|
228
|
+
"claude-opus-4.1": {
|
|
229
|
+
model_id: "us.anthropic.claude-opus-4-1-20250805-v1:0",
|
|
230
|
+
display_name: "Claude Opus 4.1",
|
|
231
|
+
provider: "bedrock",
|
|
232
|
+
context_window: 2e5,
|
|
233
|
+
max_output_tokens: 32e3
|
|
234
|
+
},
|
|
235
|
+
"claude-opus-4": {
|
|
236
|
+
model_id: "us.anthropic.claude-opus-4-20250514-v1:0",
|
|
237
|
+
display_name: "Claude Opus 4",
|
|
238
|
+
provider: "bedrock",
|
|
239
|
+
context_window: 2e5,
|
|
240
|
+
max_output_tokens: 32e3
|
|
241
|
+
},
|
|
212
242
|
"claude-sonnet-4.5": {
|
|
213
243
|
model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
214
244
|
display_name: "Claude Sonnet 4.5",
|
|
@@ -232,10 +262,24 @@ var DEFAULT_CONFIG = {
|
|
|
232
262
|
},
|
|
233
263
|
"gpt-4o": {
|
|
234
264
|
model_id: "gpt-4o",
|
|
235
|
-
display_name: "GPT-4o
|
|
236
|
-
provider: "
|
|
265
|
+
display_name: "GPT-4o",
|
|
266
|
+
provider: "openai-compatible",
|
|
237
267
|
context_window: 128e3,
|
|
238
268
|
max_output_tokens: 4096
|
|
269
|
+
},
|
|
270
|
+
"deepseek-r1:8b": {
|
|
271
|
+
model_id: "deepseek-r1:8b",
|
|
272
|
+
display_name: "DeepSeek R1 8B (Ollama)",
|
|
273
|
+
provider: "openai-compatible",
|
|
274
|
+
context_window: 128e3,
|
|
275
|
+
max_output_tokens: 8192
|
|
276
|
+
},
|
|
277
|
+
"gemma3:12b": {
|
|
278
|
+
model_id: "gemma3:12b",
|
|
279
|
+
display_name: "Gemma 3 12B (Ollama)",
|
|
280
|
+
provider: "openai-compatible",
|
|
281
|
+
context_window: 128e3,
|
|
282
|
+
max_output_tokens: 8192
|
|
239
283
|
}
|
|
240
284
|
},
|
|
241
285
|
defaults: {
|
|
@@ -272,7 +316,6 @@ function toAgentConfig(userAgent) {
|
|
|
272
316
|
endpoint: userAgent.endpoint,
|
|
273
317
|
description: userAgent.description,
|
|
274
318
|
enabled: userAgent.enabled ?? true,
|
|
275
|
-
models: userAgent.models,
|
|
276
319
|
headers: userAgent.headers ?? {},
|
|
277
320
|
useTraces: userAgent.useTraces ?? false,
|
|
278
321
|
connectorType: userAgent.connectorType,
|
|
@@ -1560,12 +1603,12 @@ var RESTConnector = class extends BaseConnector {
|
|
|
1560
1603
|
};
|
|
1561
1604
|
var restConnector = new RESTConnector();
|
|
1562
1605
|
|
|
1563
|
-
// services/connectors/
|
|
1564
|
-
var
|
|
1606
|
+
// services/connectors/openai-compatible/OpenAICompatibleConnector.ts
|
|
1607
|
+
var OpenAICompatibleConnector = class extends BaseConnector {
|
|
1565
1608
|
constructor() {
|
|
1566
1609
|
super(...arguments);
|
|
1567
|
-
this.type = "
|
|
1568
|
-
this.name = "
|
|
1610
|
+
this.type = "openai-compatible";
|
|
1611
|
+
this.name = "OpenAI-compatible";
|
|
1569
1612
|
this.supportsStreaming = false;
|
|
1570
1613
|
}
|
|
1571
1614
|
/**
|
|
@@ -1606,7 +1649,7 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1606
1649
|
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
1607
1650
|
const payload = request.payload || this.buildPayload(request);
|
|
1608
1651
|
const headers = this.buildAuthHeaders(auth);
|
|
1609
|
-
this.debug("Executing
|
|
1652
|
+
this.debug("Executing OpenAI-compatible request");
|
|
1610
1653
|
this.debug("Endpoint:", endpoint);
|
|
1611
1654
|
this.debug("Model:", payload.model);
|
|
1612
1655
|
const response = await fetch(endpoint, {
|
|
@@ -1619,7 +1662,7 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1619
1662
|
});
|
|
1620
1663
|
if (!response.ok) {
|
|
1621
1664
|
const errorText = await response.text();
|
|
1622
|
-
throw new Error(`
|
|
1665
|
+
throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
|
|
1623
1666
|
}
|
|
1624
1667
|
const data = await response.json();
|
|
1625
1668
|
onRawEvent?.(data);
|
|
@@ -1670,13 +1713,13 @@ var LiteLLMConnector = class extends BaseConnector {
|
|
|
1670
1713
|
return steps;
|
|
1671
1714
|
}
|
|
1672
1715
|
};
|
|
1673
|
-
var
|
|
1716
|
+
var openaiCompatibleConnector = new OpenAICompatibleConnector();
|
|
1674
1717
|
|
|
1675
1718
|
// services/connectors/index.ts
|
|
1676
1719
|
connectorRegistry.register(aguiStreamingConnector);
|
|
1677
1720
|
connectorRegistry.register(mockConnector);
|
|
1678
1721
|
connectorRegistry.register(restConnector);
|
|
1679
|
-
connectorRegistry.register(
|
|
1722
|
+
connectorRegistry.register(openaiCompatibleConnector);
|
|
1680
1723
|
console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
|
|
1681
1724
|
|
|
1682
1725
|
// services/connectors/subprocess/SubprocessConnector.ts
|
|
@@ -2043,7 +2086,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
2043
2086
|
this.isInThinking = false;
|
|
2044
2087
|
}
|
|
2045
2088
|
/**
|
|
2046
|
-
*
|
|
2089
|
+
* Build CLI args from ClaudeCodeConnectorConfig
|
|
2090
|
+
*/
|
|
2091
|
+
buildConfigArgs(config) {
|
|
2092
|
+
const args = [];
|
|
2093
|
+
if (config.dangerouslySkipPermissions) {
|
|
2094
|
+
args.push("--dangerously-skip-permissions");
|
|
2095
|
+
}
|
|
2096
|
+
if (config.systemPrompt) {
|
|
2097
|
+
args.push("--system-prompt", config.systemPrompt);
|
|
2098
|
+
} else if (config.appendSystemPrompt) {
|
|
2099
|
+
args.push("--append-system-prompt", config.appendSystemPrompt);
|
|
2100
|
+
}
|
|
2101
|
+
if (config.allowedTools?.length) {
|
|
2102
|
+
args.push("--allowed-tools", ...config.allowedTools);
|
|
2103
|
+
}
|
|
2104
|
+
if (config.disallowedTools?.length) {
|
|
2105
|
+
args.push("--disallowed-tools", ...config.disallowedTools);
|
|
2106
|
+
}
|
|
2107
|
+
if (config.mcpConfigPath) {
|
|
2108
|
+
args.push("--mcp-config", config.mcpConfigPath);
|
|
2109
|
+
} else if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
2110
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: config.mcpServers }));
|
|
2111
|
+
}
|
|
2112
|
+
if (config.strictMcpConfig) {
|
|
2113
|
+
args.push("--strict-mcp-config");
|
|
2114
|
+
}
|
|
2115
|
+
if (config.additionalArgs) {
|
|
2116
|
+
args.push(...config.additionalArgs);
|
|
2117
|
+
}
|
|
2118
|
+
return args;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Override execute to reset state and apply connectorConfig
|
|
2047
2122
|
*/
|
|
2048
2123
|
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
2049
2124
|
this.debug("========== execute() STARTED ==========");
|
|
@@ -2051,11 +2126,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
2051
2126
|
this.debug("Test case:", request.testCase.name);
|
|
2052
2127
|
this.debug("Config:", this["config"]);
|
|
2053
2128
|
this.resetState();
|
|
2054
|
-
this.
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2129
|
+
const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
|
|
2130
|
+
const originalArgs = this.config.args ? [...this.config.args] : [];
|
|
2131
|
+
const originalInputMode = this.config.inputMode;
|
|
2132
|
+
const originalTimeout = this.config.timeout;
|
|
2133
|
+
const originalWorkingDir = this.config.workingDir;
|
|
2134
|
+
const ccConfig = request.connectorConfig;
|
|
2135
|
+
if (ccConfig) {
|
|
2136
|
+
this.debug("Applying connectorConfig:", Object.keys(ccConfig));
|
|
2137
|
+
if (ccConfig.env) {
|
|
2138
|
+
this.config.env = { ...this.config.env, ...ccConfig.env };
|
|
2139
|
+
}
|
|
2140
|
+
if (ccConfig.usePromptArg) {
|
|
2141
|
+
this.config.inputMode = "arg";
|
|
2142
|
+
}
|
|
2143
|
+
if (ccConfig.timeout !== void 0) {
|
|
2144
|
+
this.config.timeout = ccConfig.timeout;
|
|
2145
|
+
}
|
|
2146
|
+
if (ccConfig.workingDir) {
|
|
2147
|
+
this.config.workingDir = ccConfig.workingDir;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
|
|
2151
|
+
this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
|
|
2152
|
+
this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
|
|
2153
|
+
}
|
|
2154
|
+
if (request.modelId) {
|
|
2155
|
+
this.config.args = [...this.config.args || [], "--model", request.modelId];
|
|
2156
|
+
this.debug("Model flag added:", request.modelId);
|
|
2157
|
+
}
|
|
2158
|
+
if (ccConfig) {
|
|
2159
|
+
const configArgs = this.buildConfigArgs(ccConfig);
|
|
2160
|
+
if (configArgs.length > 0) {
|
|
2161
|
+
this.config.args = [...this.config.args || [], ...configArgs];
|
|
2162
|
+
this.debug("Config args added:", configArgs);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
this.debug("State reset, calling super.execute()...");
|
|
2167
|
+
const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
|
|
2168
|
+
this.debug("super.execute() returned with", result.trajectory.length, "steps");
|
|
2169
|
+
this.debug("========== execute() COMPLETED ==========");
|
|
2170
|
+
return result;
|
|
2171
|
+
} finally {
|
|
2172
|
+
this.config.env = originalEnv;
|
|
2173
|
+
this.config.args = originalArgs;
|
|
2174
|
+
this.config.inputMode = originalInputMode;
|
|
2175
|
+
this.config.timeout = originalTimeout;
|
|
2176
|
+
this.config.workingDir = originalWorkingDir;
|
|
2177
|
+
}
|
|
2059
2178
|
}
|
|
2060
2179
|
/**
|
|
2061
2180
|
* Health check - verify claude command exists
|
|
@@ -2309,6 +2428,12 @@ function createServerCleanup(result, isCI) {
|
|
|
2309
2428
|
}
|
|
2310
2429
|
|
|
2311
2430
|
// cli/utils/apiClient.ts
|
|
2431
|
+
var ServerError = class extends Error {
|
|
2432
|
+
constructor(message) {
|
|
2433
|
+
super(message);
|
|
2434
|
+
this.name = "ServerError";
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2312
2437
|
var ApiClient = class {
|
|
2313
2438
|
constructor(baseUrl) {
|
|
2314
2439
|
this.baseUrl = baseUrl;
|
|
@@ -2424,7 +2549,7 @@ var ApiClient = class {
|
|
|
2424
2549
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
2425
2550
|
finalRun = event.run;
|
|
2426
2551
|
} else if (event.type === "error") {
|
|
2427
|
-
throw new
|
|
2552
|
+
throw new ServerError(event.error);
|
|
2428
2553
|
}
|
|
2429
2554
|
} catch (e) {
|
|
2430
2555
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2434,6 +2559,9 @@ var ApiClient = class {
|
|
|
2434
2559
|
}
|
|
2435
2560
|
}
|
|
2436
2561
|
} catch (streamError) {
|
|
2562
|
+
if (streamError instanceof ServerError) {
|
|
2563
|
+
throw streamError;
|
|
2564
|
+
}
|
|
2437
2565
|
if (runId) {
|
|
2438
2566
|
console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
|
|
2439
2567
|
console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
|
|
@@ -2671,7 +2799,7 @@ var ApiClient = class {
|
|
|
2671
2799
|
}
|
|
2672
2800
|
const response = await this.listTestCasesWithMeta();
|
|
2673
2801
|
return response.data.find(
|
|
2674
|
-
(tc) => tc.name
|
|
2802
|
+
(tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
|
|
2675
2803
|
) || null;
|
|
2676
2804
|
}
|
|
2677
2805
|
/**
|
|
@@ -2719,7 +2847,7 @@ var ApiClient = class {
|
|
|
2719
2847
|
if (event.type === "completed") {
|
|
2720
2848
|
result = event.report;
|
|
2721
2849
|
} else if (event.type === "error") {
|
|
2722
|
-
throw new
|
|
2850
|
+
throw new ServerError(event.error);
|
|
2723
2851
|
}
|
|
2724
2852
|
} catch (e) {
|
|
2725
2853
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2769,12 +2897,53 @@ var ApiClient = class {
|
|
|
2769
2897
|
}
|
|
2770
2898
|
return res.json();
|
|
2771
2899
|
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Fetch traces from OpenSearch with optional filters
|
|
2902
|
+
*/
|
|
2903
|
+
async fetchTraces(params) {
|
|
2904
|
+
const res = await fetch(`${this.baseUrl}/api/traces`, {
|
|
2905
|
+
method: "POST",
|
|
2906
|
+
headers: { "Content-Type": "application/json" },
|
|
2907
|
+
body: JSON.stringify(params)
|
|
2908
|
+
});
|
|
2909
|
+
if (!res.ok) {
|
|
2910
|
+
const errorBody = await res.text();
|
|
2911
|
+
let errorMessage;
|
|
2912
|
+
try {
|
|
2913
|
+
const parsed = JSON.parse(errorBody);
|
|
2914
|
+
errorMessage = parsed.error || errorBody;
|
|
2915
|
+
} catch {
|
|
2916
|
+
errorMessage = errorBody;
|
|
2917
|
+
}
|
|
2918
|
+
throw new Error(`Failed to fetch traces: ${errorMessage}`);
|
|
2919
|
+
}
|
|
2920
|
+
return res.json();
|
|
2921
|
+
}
|
|
2772
2922
|
};
|
|
2773
2923
|
|
|
2774
|
-
// cli/
|
|
2924
|
+
// cli/utils/formatOutput.ts
|
|
2925
|
+
var OUTPUT_FORMAT_DESCRIPTION = "Output format: table, json, markdown";
|
|
2926
|
+
function formatMarkdownTable(headers, rows) {
|
|
2927
|
+
const separator = headers.map(() => "---");
|
|
2928
|
+
const lines = [
|
|
2929
|
+
`| ${headers.join(" | ")} |`,
|
|
2930
|
+
`| ${separator.join(" | ")} |`,
|
|
2931
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
2932
|
+
];
|
|
2933
|
+
return lines.join("\n");
|
|
2934
|
+
}
|
|
2775
2935
|
function formatJson(data) {
|
|
2776
2936
|
return JSON.stringify(data, null, 2);
|
|
2777
2937
|
}
|
|
2938
|
+
function parseOutputFormat(format) {
|
|
2939
|
+
const normalized = format.toLowerCase();
|
|
2940
|
+
if (normalized === "table" || normalized === "json" || normalized === "markdown" || normalized === "md") {
|
|
2941
|
+
return normalized === "md" ? "markdown" : normalized;
|
|
2942
|
+
}
|
|
2943
|
+
return "table";
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// cli/commands/list.ts
|
|
2778
2947
|
function displayStorageWarnings(meta) {
|
|
2779
2948
|
if (!meta.storageConfigured) {
|
|
2780
2949
|
console.log(chalk.yellow("\n \u26A0 Storage not configured"));
|
|
@@ -2795,25 +2964,24 @@ async function listAgents(format, config) {
|
|
|
2795
2964
|
console.log(formatJson(agents));
|
|
2796
2965
|
return;
|
|
2797
2966
|
}
|
|
2967
|
+
const headers = ["Key", "Name", "Connector", "Endpoint"];
|
|
2968
|
+
const rows = agents.map((agent) => [
|
|
2969
|
+
agent.key,
|
|
2970
|
+
agent.name,
|
|
2971
|
+
agent.connectorType || "agui-streaming",
|
|
2972
|
+
agent.endpoint.substring(0, 47) + (agent.endpoint.length > 47 ? "..." : "")
|
|
2973
|
+
]);
|
|
2974
|
+
if (format === "markdown") {
|
|
2975
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2798
2978
|
const table = new Table({
|
|
2799
|
-
head:
|
|
2800
|
-
|
|
2801
|
-
chalk.cyan("Name"),
|
|
2802
|
-
chalk.cyan("Connector"),
|
|
2803
|
-
chalk.cyan("Models"),
|
|
2804
|
-
chalk.cyan("Endpoint")
|
|
2805
|
-
],
|
|
2806
|
-
colWidths: [15, 20, 15, 25, 40],
|
|
2979
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2980
|
+
colWidths: [15, 20, 15, 50],
|
|
2807
2981
|
wordWrap: true
|
|
2808
2982
|
});
|
|
2809
|
-
for (const
|
|
2810
|
-
table.push(
|
|
2811
|
-
agent.key,
|
|
2812
|
-
agent.name,
|
|
2813
|
-
agent.connectorType || "agui-streaming",
|
|
2814
|
-
agent.models.slice(0, 3).join(", ") + (agent.models.length > 3 ? "..." : ""),
|
|
2815
|
-
agent.endpoint.substring(0, 37) + (agent.endpoint.length > 37 ? "..." : "")
|
|
2816
|
-
]);
|
|
2983
|
+
for (const row of rows) {
|
|
2984
|
+
table.push(row);
|
|
2817
2985
|
}
|
|
2818
2986
|
console.log(chalk.bold("\nAvailable Agents:\n"));
|
|
2819
2987
|
console.log(table.toString());
|
|
@@ -2835,31 +3003,35 @@ async function listTestCases(format, config) {
|
|
|
2835
3003
|
try {
|
|
2836
3004
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2837
3005
|
const response = await client.listTestCasesWithMeta();
|
|
2838
|
-
|
|
3006
|
+
if (format !== "markdown") {
|
|
3007
|
+
displayStorageWarnings(response.meta);
|
|
3008
|
+
}
|
|
2839
3009
|
if (format === "json") {
|
|
2840
3010
|
console.log(formatJson(response));
|
|
2841
3011
|
return;
|
|
2842
3012
|
}
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
chalk.cyan("ID"),
|
|
2846
|
-
chalk.cyan("Name"),
|
|
2847
|
-
chalk.cyan("Labels"),
|
|
2848
|
-
chalk.cyan("Version"),
|
|
2849
|
-
chalk.cyan("Source")
|
|
2850
|
-
],
|
|
2851
|
-
colWidths: [25, 28, 28, 10, 10],
|
|
2852
|
-
wordWrap: true
|
|
2853
|
-
});
|
|
2854
|
-
for (const tc of response.data) {
|
|
3013
|
+
const headers = ["ID", "Name", "Labels", "Version", "Source"];
|
|
3014
|
+
const rows = response.data.map((tc) => {
|
|
2855
3015
|
const isDemo = tc.id.startsWith("demo-");
|
|
2856
|
-
|
|
3016
|
+
return [
|
|
2857
3017
|
tc.id,
|
|
2858
3018
|
tc.name,
|
|
2859
3019
|
tc.labels?.slice(0, 3).join(", ") || "",
|
|
2860
3020
|
`v${tc.currentVersion || 1}`,
|
|
2861
|
-
isDemo ?
|
|
2862
|
-
]
|
|
3021
|
+
isDemo ? "Sample" : "Stored"
|
|
3022
|
+
];
|
|
3023
|
+
});
|
|
3024
|
+
if (format === "markdown") {
|
|
3025
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const table = new Table({
|
|
3029
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3030
|
+
colWidths: [25, 28, 28, 10, 10],
|
|
3031
|
+
wordWrap: true
|
|
3032
|
+
});
|
|
3033
|
+
for (const row of rows) {
|
|
3034
|
+
table.push(row);
|
|
2863
3035
|
}
|
|
2864
3036
|
console.log(chalk.bold("\nAvailable Test Cases:\n"));
|
|
2865
3037
|
console.log(table.toString());
|
|
@@ -2888,31 +3060,35 @@ async function listBenchmarks(format, config) {
|
|
|
2888
3060
|
try {
|
|
2889
3061
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2890
3062
|
const response = await client.listBenchmarksWithMeta();
|
|
2891
|
-
|
|
3063
|
+
if (format !== "markdown") {
|
|
3064
|
+
displayStorageWarnings(response.meta);
|
|
3065
|
+
}
|
|
2892
3066
|
if (format === "json") {
|
|
2893
3067
|
console.log(formatJson(response));
|
|
2894
3068
|
return;
|
|
2895
3069
|
}
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
chalk.cyan("ID"),
|
|
2899
|
-
chalk.cyan("Name"),
|
|
2900
|
-
chalk.cyan("Test Cases"),
|
|
2901
|
-
chalk.cyan("Created"),
|
|
2902
|
-
chalk.cyan("Source")
|
|
2903
|
-
],
|
|
2904
|
-
colWidths: [28, 28, 12, 22, 10],
|
|
2905
|
-
wordWrap: true
|
|
2906
|
-
});
|
|
2907
|
-
for (const b of response.data) {
|
|
3070
|
+
const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
|
|
3071
|
+
const rows = response.data.map((b) => {
|
|
2908
3072
|
const isDemo = b.id.startsWith("demo-");
|
|
2909
|
-
|
|
3073
|
+
return [
|
|
2910
3074
|
b.id,
|
|
2911
3075
|
b.name,
|
|
2912
3076
|
b.testCaseIds.length.toString(),
|
|
2913
3077
|
new Date(b.createdAt).toLocaleDateString(),
|
|
2914
|
-
isDemo ?
|
|
2915
|
-
]
|
|
3078
|
+
isDemo ? "Sample" : "Stored"
|
|
3079
|
+
];
|
|
3080
|
+
});
|
|
3081
|
+
if (format === "markdown") {
|
|
3082
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const table = new Table({
|
|
3086
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3087
|
+
colWidths: [28, 28, 12, 22, 10],
|
|
3088
|
+
wordWrap: true
|
|
3089
|
+
});
|
|
3090
|
+
for (const row of rows) {
|
|
3091
|
+
table.push(row);
|
|
2916
3092
|
}
|
|
2917
3093
|
console.log(chalk.bold("\nAvailable Benchmarks:\n"));
|
|
2918
3094
|
console.log(table.toString());
|
|
@@ -2949,19 +3125,25 @@ function listConnectors(format) {
|
|
|
2949
3125
|
console.log(formatJson(connectors));
|
|
2950
3126
|
return;
|
|
2951
3127
|
}
|
|
3128
|
+
const headers = ["Type", "Name", "Streaming"];
|
|
3129
|
+
const rows = connectors.map((c) => [
|
|
3130
|
+
c.type,
|
|
3131
|
+
c.name,
|
|
3132
|
+
c.streaming ? "Yes" : "No"
|
|
3133
|
+
]);
|
|
3134
|
+
if (format === "markdown") {
|
|
3135
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
2952
3138
|
const table = new Table({
|
|
2953
|
-
head:
|
|
2954
|
-
chalk.cyan("Type"),
|
|
2955
|
-
chalk.cyan("Name"),
|
|
2956
|
-
chalk.cyan("Streaming")
|
|
2957
|
-
],
|
|
3139
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2958
3140
|
colWidths: [20, 25, 12]
|
|
2959
3141
|
});
|
|
2960
|
-
for (const
|
|
3142
|
+
for (const row of rows) {
|
|
2961
3143
|
table.push([
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3144
|
+
row[0],
|
|
3145
|
+
row[1],
|
|
3146
|
+
row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
|
|
2965
3147
|
]);
|
|
2966
3148
|
}
|
|
2967
3149
|
console.log(chalk.bold("\nRegistered Connectors:\n"));
|
|
@@ -2980,23 +3162,24 @@ async function listModels(format, config) {
|
|
|
2980
3162
|
console.log(formatJson(models));
|
|
2981
3163
|
return;
|
|
2982
3164
|
}
|
|
3165
|
+
const headers = ["Key", "Display Name", "Provider", "Context"];
|
|
3166
|
+
const rows = models.map((m) => [
|
|
3167
|
+
m.key,
|
|
3168
|
+
m.display_name || m.key,
|
|
3169
|
+
m.provider || "bedrock",
|
|
3170
|
+
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
3171
|
+
]);
|
|
3172
|
+
if (format === "markdown") {
|
|
3173
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
2983
3176
|
const table = new Table({
|
|
2984
|
-
head:
|
|
2985
|
-
chalk.cyan("Key"),
|
|
2986
|
-
chalk.cyan("Display Name"),
|
|
2987
|
-
chalk.cyan("Provider"),
|
|
2988
|
-
chalk.cyan("Context")
|
|
2989
|
-
],
|
|
3177
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2990
3178
|
colWidths: [25, 30, 12, 12],
|
|
2991
3179
|
wordWrap: true
|
|
2992
3180
|
});
|
|
2993
|
-
for (const
|
|
2994
|
-
table.push(
|
|
2995
|
-
m.key,
|
|
2996
|
-
m.display_name || m.key,
|
|
2997
|
-
m.provider || "bedrock",
|
|
2998
|
-
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
2999
|
-
]);
|
|
3181
|
+
for (const row of rows) {
|
|
3182
|
+
table.push(row);
|
|
3000
3183
|
}
|
|
3001
3184
|
console.log(chalk.bold("\nAvailable Models:\n"));
|
|
3002
3185
|
console.log(table.toString());
|
|
@@ -3013,8 +3196,8 @@ async function listModels(format, config) {
|
|
|
3013
3196
|
}
|
|
3014
3197
|
}
|
|
3015
3198
|
function createListCommand() {
|
|
3016
|
-
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>",
|
|
3017
|
-
const format = options.output;
|
|
3199
|
+
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").action(async (resource, options) => {
|
|
3200
|
+
const format = parseOutputFormat(options.output);
|
|
3018
3201
|
const config = await loadConfig();
|
|
3019
3202
|
for (const connector of config.connectors) {
|
|
3020
3203
|
connectorRegistry.register(connector);
|
|
@@ -3058,8 +3241,8 @@ function findAgent(identifier, config) {
|
|
|
3058
3241
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
3059
3242
|
);
|
|
3060
3243
|
}
|
|
3061
|
-
function getDefaultModel(
|
|
3062
|
-
return
|
|
3244
|
+
function getDefaultModel(config) {
|
|
3245
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
3063
3246
|
}
|
|
3064
3247
|
async function commandExists(command) {
|
|
3065
3248
|
const { execSync: execSync2 } = await import("child_process");
|
|
@@ -3113,26 +3296,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
|
|
|
3113
3296
|
throw error;
|
|
3114
3297
|
}
|
|
3115
3298
|
}
|
|
3116
|
-
function
|
|
3299
|
+
function buildResultRows(results) {
|
|
3300
|
+
return results.map((r) => {
|
|
3301
|
+
if (!r.report) {
|
|
3302
|
+
return [r.agent.name, "ERROR", "-", "-", "-"];
|
|
3303
|
+
}
|
|
3304
|
+
const status = r.report.passFailStatus === "passed" ? "PASSED" : r.report.passFailStatus === "failed" ? "FAILED" : r.report.status;
|
|
3305
|
+
return [
|
|
3306
|
+
r.agent.name,
|
|
3307
|
+
status,
|
|
3308
|
+
r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
|
|
3309
|
+
r.report.trajectorySteps.toString(),
|
|
3310
|
+
r.report.id?.substring(0, 27) + "..." || "-"
|
|
3311
|
+
];
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
function displayResults(results, format) {
|
|
3315
|
+
const headers = ["Agent", "Status", "Accuracy", "Steps", "Report ID"];
|
|
3316
|
+
const rows = buildResultRows(results);
|
|
3317
|
+
if (format === "markdown") {
|
|
3318
|
+
console.log("\n");
|
|
3319
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3117
3322
|
const table = new Table2({
|
|
3118
|
-
head:
|
|
3119
|
-
chalk2.cyan("Agent"),
|
|
3120
|
-
chalk2.cyan("Status"),
|
|
3121
|
-
chalk2.cyan("Accuracy"),
|
|
3122
|
-
chalk2.cyan("Steps"),
|
|
3123
|
-
chalk2.cyan("Report ID")
|
|
3124
|
-
],
|
|
3323
|
+
head: headers.map((h) => chalk2.cyan(h)),
|
|
3125
3324
|
colWidths: [20, 12, 12, 10, 30]
|
|
3126
3325
|
});
|
|
3127
3326
|
for (const r of results) {
|
|
3128
3327
|
if (!r.report) {
|
|
3129
|
-
table.push([
|
|
3130
|
-
r.agent.name,
|
|
3131
|
-
chalk2.red("ERROR"),
|
|
3132
|
-
"-",
|
|
3133
|
-
"-",
|
|
3134
|
-
"-"
|
|
3135
|
-
]);
|
|
3328
|
+
table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
|
|
3136
3329
|
continue;
|
|
3137
3330
|
}
|
|
3138
3331
|
const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
|
|
@@ -3148,7 +3341,7 @@ function displayTableResults(results) {
|
|
|
3148
3341
|
console.log(table.toString());
|
|
3149
3342
|
}
|
|
3150
3343
|
function createRunCommand() {
|
|
3151
|
-
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>",
|
|
3344
|
+
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("-v, --verbose", "Show detailed trajectory output").action(async (options) => {
|
|
3152
3345
|
console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
|
|
3153
3346
|
const config = await loadConfig();
|
|
3154
3347
|
for (const connector of config.connectors) {
|
|
@@ -3186,7 +3379,7 @@ function createRunCommand() {
|
|
|
3186
3379
|
console.log("");
|
|
3187
3380
|
const results = [];
|
|
3188
3381
|
for (const agent of agents) {
|
|
3189
|
-
const modelId = options.model || getDefaultModel(
|
|
3382
|
+
const modelId = options.model || getDefaultModel(config);
|
|
3190
3383
|
const validationError = await validateAgentRequirements(agent);
|
|
3191
3384
|
if (validationError) {
|
|
3192
3385
|
console.error(chalk2.red(` Error: ${validationError}`));
|
|
@@ -3197,17 +3390,27 @@ function createRunCommand() {
|
|
|
3197
3390
|
const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
|
|
3198
3391
|
results.push({ agent, report });
|
|
3199
3392
|
} catch (error) {
|
|
3200
|
-
|
|
3393
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3394
|
+
console.error(chalk2.red(` Error running ${agent.name}: ${errorMsg}`));
|
|
3395
|
+
const lowerError = errorMsg.toLowerCase();
|
|
3396
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3397
|
+
console.log(chalk2.gray(` Hint: Authentication issue. Check agent-health.config.ts (headers, hooks.beforeRequest, or credentials).`));
|
|
3398
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3399
|
+
console.log(chalk2.gray(` Hint: Cannot reach agent endpoint. Run: npx @opensearch-project/agent-health doctor`));
|
|
3400
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3401
|
+
console.log(chalk2.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3402
|
+
}
|
|
3201
3403
|
results.push({ agent, report: null });
|
|
3202
3404
|
}
|
|
3203
3405
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3406
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3407
|
+
if (outputFormat === "json") {
|
|
3408
|
+
console.log(formatJson(results.map((r) => ({
|
|
3206
3409
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3207
3410
|
report: r.report
|
|
3208
|
-
}))
|
|
3411
|
+
}))));
|
|
3209
3412
|
} else {
|
|
3210
|
-
|
|
3413
|
+
displayResults(results, outputFormat);
|
|
3211
3414
|
}
|
|
3212
3415
|
} catch (error) {
|
|
3213
3416
|
console.error(chalk2.red(`
|
|
@@ -3354,8 +3557,8 @@ function findAgent2(identifier, config) {
|
|
|
3354
3557
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
3355
3558
|
);
|
|
3356
3559
|
}
|
|
3357
|
-
function getDefaultModel2(
|
|
3358
|
-
return
|
|
3560
|
+
function getDefaultModel2(config) {
|
|
3561
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
3359
3562
|
}
|
|
3360
3563
|
function isFilePath(value) {
|
|
3361
3564
|
return value.toLowerCase().endsWith(".json");
|
|
@@ -3391,7 +3594,7 @@ async function fetchReportsForRun(api, run) {
|
|
|
3391
3594
|
);
|
|
3392
3595
|
return reportsMap;
|
|
3393
3596
|
}
|
|
3394
|
-
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
3597
|
+
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
|
|
3395
3598
|
const results = {
|
|
3396
3599
|
agent,
|
|
3397
3600
|
passed: 0,
|
|
@@ -3406,7 +3609,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3406
3609
|
{
|
|
3407
3610
|
name: `CLI Run - ${agent.name}`,
|
|
3408
3611
|
agentKey: agent.key,
|
|
3409
|
-
modelId
|
|
3612
|
+
modelId,
|
|
3613
|
+
...concurrency && concurrency > 1 ? { concurrency } : {}
|
|
3410
3614
|
},
|
|
3411
3615
|
(event) => {
|
|
3412
3616
|
if (event.type === "started") {
|
|
@@ -3415,9 +3619,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3415
3619
|
const current = event.currentTestCaseIndex + 1;
|
|
3416
3620
|
const testCaseName = event.currentTestCase?.name || `Test ${current}`;
|
|
3417
3621
|
spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
|
|
3418
|
-
if (
|
|
3622
|
+
if (event.result) {
|
|
3419
3623
|
const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
|
|
3420
3624
|
spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
|
|
3625
|
+
if (verbose && event.result.status === "failed" && event.result.error) {
|
|
3626
|
+
spinner.info(`${agent.name}: ${testCaseName} ${chalk3.red("\u2717")} - ${event.result.error}`);
|
|
3627
|
+
spinner.start(`${agent.name}: (${current}/${totalTestCases})`);
|
|
3628
|
+
}
|
|
3421
3629
|
}
|
|
3422
3630
|
}
|
|
3423
3631
|
}
|
|
@@ -3444,6 +3652,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3444
3652
|
}
|
|
3445
3653
|
} catch (error) {
|
|
3446
3654
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3655
|
+
const isServerError = error instanceof ServerError;
|
|
3447
3656
|
if (startedRunId) {
|
|
3448
3657
|
results.runId = startedRunId;
|
|
3449
3658
|
try {
|
|
@@ -3472,29 +3681,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3472
3681
|
}
|
|
3473
3682
|
return results;
|
|
3474
3683
|
}
|
|
3684
|
+
if (run.status === "failed") {
|
|
3685
|
+
const runError = run.error || errorMessage;
|
|
3686
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${runError}`);
|
|
3687
|
+
if (stats.passed > 0 || stats.failed > 0) {
|
|
3688
|
+
console.log(chalk3.gray(` Partial results: ${stats.passed} passed, ${stats.failed} failed out of ${stats.total}`));
|
|
3689
|
+
}
|
|
3690
|
+
return results;
|
|
3691
|
+
}
|
|
3475
3692
|
}
|
|
3476
3693
|
} catch {
|
|
3477
3694
|
}
|
|
3478
3695
|
}
|
|
3479
|
-
|
|
3480
|
-
if (isStreamError && startedRunId) {
|
|
3481
|
-
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3482
|
-
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3483
|
-
} else {
|
|
3696
|
+
if (isServerError) {
|
|
3484
3697
|
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3698
|
+
} else {
|
|
3699
|
+
const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
|
|
3700
|
+
if (isStreamError && startedRunId) {
|
|
3701
|
+
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3702
|
+
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3703
|
+
} else {
|
|
3704
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
const lowerError = errorMessage.toLowerCase();
|
|
3708
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3709
|
+
console.log(chalk3.gray(` Hint: This looks like an authentication issue. Check your agent-health.config.ts`));
|
|
3710
|
+
console.log(chalk3.gray(` (headers, hooks.beforeRequest, or credentials) and re-run.`));
|
|
3711
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3712
|
+
console.log(chalk3.gray(` Hint: Could not connect to the agent endpoint. Verify the endpoint in agent-health.config.ts`));
|
|
3713
|
+
console.log(chalk3.gray(` is reachable: npx @opensearch-project/agent-health doctor`));
|
|
3714
|
+
} else if (lowerError.includes("not found") || lowerError.includes("agent not found")) {
|
|
3715
|
+
console.log(chalk3.gray(` Hint: Agent key not found. List available agents: npx @opensearch-project/agent-health list agents`));
|
|
3716
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3717
|
+
console.log(chalk3.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3718
|
+
console.log(chalk3.gray(` Check the hook logic and any external services it calls.`));
|
|
3719
|
+
}
|
|
3720
|
+
if (errorMessage !== "terminated") {
|
|
3721
|
+
console.log(chalk3.gray(` Debug: Run with DEBUG=true for verbose server logs`));
|
|
3485
3722
|
}
|
|
3486
3723
|
}
|
|
3487
3724
|
return results;
|
|
3488
3725
|
}
|
|
3489
|
-
function
|
|
3726
|
+
function buildSummaryRows(allResults, totalTestCases) {
|
|
3727
|
+
return allResults.map((results) => {
|
|
3728
|
+
const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
|
|
3729
|
+
return [
|
|
3730
|
+
results.agent.name,
|
|
3731
|
+
results.passed.toString(),
|
|
3732
|
+
results.failed.toString(),
|
|
3733
|
+
`${passRate.toFixed(0)}%`,
|
|
3734
|
+
results.run?.id || results.runId || "N/A"
|
|
3735
|
+
];
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
function displaySummary(allResults, totalTestCases, format) {
|
|
3739
|
+
const headers = ["Agent", "Passed", "Failed", "Pass Rate", "Run ID"];
|
|
3740
|
+
const rows = buildSummaryRows(allResults, totalTestCases);
|
|
3741
|
+
if (format === "markdown") {
|
|
3742
|
+
console.log("\n");
|
|
3743
|
+
console.log("## Benchmark Summary\n");
|
|
3744
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3490
3747
|
const table = new Table3({
|
|
3491
|
-
head:
|
|
3492
|
-
chalk3.cyan("Agent"),
|
|
3493
|
-
chalk3.cyan("Passed"),
|
|
3494
|
-
chalk3.cyan("Failed"),
|
|
3495
|
-
chalk3.cyan("Pass Rate"),
|
|
3496
|
-
chalk3.cyan("Run ID")
|
|
3497
|
-
],
|
|
3748
|
+
head: headers.map((h) => chalk3.cyan(h)),
|
|
3498
3749
|
colWidths: [25, 10, 10, 12, 35]
|
|
3499
3750
|
});
|
|
3500
3751
|
for (const results of allResults) {
|
|
@@ -3565,7 +3816,7 @@ function createBenchmarkCommand() {
|
|
|
3565
3816
|
"Agent key (can be specified multiple times)",
|
|
3566
3817
|
(val, arr) => [...arr, val],
|
|
3567
3818
|
[]
|
|
3568
|
-
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>",
|
|
3819
|
+
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("--export <path>", "Export results to file").option("--format <type>", "Report format for --export: json (default), html, pdf", "json").option("-c, --concurrency <n>", "Number of test cases to run in parallel (default: 1)", "1").option("-v, --verbose", "Show detailed output").option("--stop-server", "Stop the server after benchmark completes (default: keep running)").action(async (options) => {
|
|
3569
3820
|
console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
|
|
3570
3821
|
const config = await loadConfig();
|
|
3571
3822
|
const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
|
|
@@ -3699,6 +3950,9 @@ function createBenchmarkCommand() {
|
|
|
3699
3950
|
console.log(chalk3.gray(` - ${a.name} (${a.key})`));
|
|
3700
3951
|
}
|
|
3701
3952
|
console.log("");
|
|
3953
|
+
console.log(chalk3.gray(" To add a custom agent, configure it in agent-health.config.ts"));
|
|
3954
|
+
console.log(chalk3.gray(" Generate one with: npx @opensearch-project/agent-health init"));
|
|
3955
|
+
console.log("");
|
|
3702
3956
|
process.exit(1);
|
|
3703
3957
|
}
|
|
3704
3958
|
agents.push(agent);
|
|
@@ -3706,19 +3960,25 @@ function createBenchmarkCommand() {
|
|
|
3706
3960
|
console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
|
|
3707
3961
|
}
|
|
3708
3962
|
console.log("");
|
|
3963
|
+
const concurrency = Math.max(1, Math.min(20, parseInt(options.concurrency, 10) || 1));
|
|
3964
|
+
if (concurrency > 1) {
|
|
3965
|
+
console.log(chalk3.gray(` Concurrency: ${concurrency}`));
|
|
3966
|
+
}
|
|
3709
3967
|
const allResults = [];
|
|
3710
3968
|
for (const agent of agents) {
|
|
3711
|
-
const modelId = options.model || getDefaultModel2(
|
|
3969
|
+
const modelId = options.model || getDefaultModel2(config);
|
|
3712
3970
|
const results = await runBenchmarkForAgent(
|
|
3713
3971
|
api,
|
|
3714
3972
|
agent,
|
|
3715
3973
|
modelId,
|
|
3716
3974
|
benchmark,
|
|
3717
|
-
options.verbose || false
|
|
3975
|
+
options.verbose || false,
|
|
3976
|
+
concurrency
|
|
3718
3977
|
);
|
|
3719
3978
|
allResults.push(results);
|
|
3720
3979
|
}
|
|
3721
|
-
|
|
3980
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3981
|
+
if (outputFormat === "json") {
|
|
3722
3982
|
const jsonOutput = allResults.map((r) => ({
|
|
3723
3983
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3724
3984
|
runId: r.run?.id || r.runId,
|
|
@@ -3727,9 +3987,9 @@ function createBenchmarkCommand() {
|
|
|
3727
3987
|
passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
|
|
3728
3988
|
results: r.run?.results
|
|
3729
3989
|
}));
|
|
3730
|
-
console.log(
|
|
3990
|
+
console.log(formatJson(jsonOutput));
|
|
3731
3991
|
} else {
|
|
3732
|
-
|
|
3992
|
+
displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
|
|
3733
3993
|
}
|
|
3734
3994
|
if (options.export) {
|
|
3735
3995
|
await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
|
|
@@ -4045,7 +4305,7 @@ function checkOpenSearchObservability() {
|
|
|
4045
4305
|
]
|
|
4046
4306
|
};
|
|
4047
4307
|
}
|
|
4048
|
-
function
|
|
4308
|
+
function displayResults2(results) {
|
|
4049
4309
|
console.log(chalk6.bold("\n Configuration Check\n"));
|
|
4050
4310
|
for (const result of results) {
|
|
4051
4311
|
const icon = {
|
|
@@ -4096,7 +4356,7 @@ function createDoctorCommand() {
|
|
|
4096
4356
|
if (options.output === "json") {
|
|
4097
4357
|
console.log(JSON.stringify(results, null, 2));
|
|
4098
4358
|
} else {
|
|
4099
|
-
|
|
4359
|
+
displayResults2(results);
|
|
4100
4360
|
}
|
|
4101
4361
|
});
|
|
4102
4362
|
return command;
|
|
@@ -4127,7 +4387,6 @@ export default defineConfig({
|
|
|
4127
4387
|
username: process.env.OPENSEARCH_USER || 'admin',
|
|
4128
4388
|
password: process.env.OPENSEARCH_PASS || 'admin',
|
|
4129
4389
|
},
|
|
4130
|
-
models: ['claude-sonnet'],
|
|
4131
4390
|
},
|
|
4132
4391
|
|
|
4133
4392
|
// Claude Code CLI agent (optional)
|
|
@@ -4144,7 +4403,6 @@ export default defineConfig({
|
|
|
4144
4403
|
},
|
|
4145
4404
|
}),
|
|
4146
4405
|
endpoint: 'claude', // Command name
|
|
4147
|
-
models: ['claude-sonnet-4'],
|
|
4148
4406
|
},
|
|
4149
4407
|
*/
|
|
4150
4408
|
],
|
|
@@ -4426,6 +4684,338 @@ function createMigrateCommand() {
|
|
|
4426
4684
|
return command;
|
|
4427
4685
|
}
|
|
4428
4686
|
|
|
4687
|
+
// cli/commands/compare-services.ts
|
|
4688
|
+
import { Command as Command9 } from "commander";
|
|
4689
|
+
import chalk9 from "chalk";
|
|
4690
|
+
function analyzeErrorPatterns(spans) {
|
|
4691
|
+
const errorSpans = spans.filter((s) => s.status === "ERROR");
|
|
4692
|
+
if (errorSpans.length === 0) {
|
|
4693
|
+
return { errorSpans, patterns: [], avgDurationMs: 0 };
|
|
4694
|
+
}
|
|
4695
|
+
const patternMap = /* @__PURE__ */ new Map();
|
|
4696
|
+
let totalDuration = 0;
|
|
4697
|
+
for (const span of errorSpans) {
|
|
4698
|
+
const errorType = extractErrorType(span);
|
|
4699
|
+
const spanName = span.name || "Unknown";
|
|
4700
|
+
const duration = span.duration || 0;
|
|
4701
|
+
const errorMsg = extractErrorMessage(span);
|
|
4702
|
+
totalDuration += duration;
|
|
4703
|
+
if (!patternMap.has(errorType)) {
|
|
4704
|
+
patternMap.set(errorType, {
|
|
4705
|
+
count: 0,
|
|
4706
|
+
spanNames: /* @__PURE__ */ new Set(),
|
|
4707
|
+
durations: [],
|
|
4708
|
+
messages: /* @__PURE__ */ new Set()
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
const pattern = patternMap.get(errorType);
|
|
4712
|
+
pattern.count++;
|
|
4713
|
+
pattern.spanNames.add(spanName);
|
|
4714
|
+
pattern.durations.push(duration);
|
|
4715
|
+
if (errorMsg) pattern.messages.add(errorMsg);
|
|
4716
|
+
}
|
|
4717
|
+
const patterns = Array.from(patternMap.entries()).map(([errorType, data]) => ({
|
|
4718
|
+
errorType,
|
|
4719
|
+
count: data.count,
|
|
4720
|
+
spanNames: Array.from(data.spanNames),
|
|
4721
|
+
avgDurationMs: data.durations.reduce((a, b) => a + b, 0) / data.durations.length,
|
|
4722
|
+
exampleMessages: Array.from(data.messages).slice(0, 3)
|
|
4723
|
+
// Top 3 examples
|
|
4724
|
+
})).sort((a, b) => b.count - a.count);
|
|
4725
|
+
const avgDurationMs = totalDuration / errorSpans.length;
|
|
4726
|
+
return { errorSpans, patterns, avgDurationMs };
|
|
4727
|
+
}
|
|
4728
|
+
function extractErrorType(span) {
|
|
4729
|
+
const attrs = span.attributes || {};
|
|
4730
|
+
if (attrs["error.type"]) return attrs["error.type"];
|
|
4731
|
+
if (attrs["exception.type"]) return attrs["exception.type"];
|
|
4732
|
+
if (attrs["http.status_code"] >= 400) return `HTTP ${attrs["http.status_code"]}`;
|
|
4733
|
+
const name = span.name || "";
|
|
4734
|
+
if (name.includes("timeout")) return "Timeout";
|
|
4735
|
+
if (name.includes("connection")) return "Connection Error";
|
|
4736
|
+
if (name.includes("auth")) return "Authentication Error";
|
|
4737
|
+
return "Unknown Error";
|
|
4738
|
+
}
|
|
4739
|
+
function extractErrorMessage(span) {
|
|
4740
|
+
const attrs = span.attributes || {};
|
|
4741
|
+
if (attrs["error.message"]) return attrs["error.message"];
|
|
4742
|
+
if (attrs["exception.message"]) return attrs["exception.message"];
|
|
4743
|
+
if (span.events && Array.isArray(span.events)) {
|
|
4744
|
+
for (const event of span.events) {
|
|
4745
|
+
if (event.name === "exception" && event.attributes?.["exception.message"]) {
|
|
4746
|
+
return event.attributes["exception.message"];
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
return null;
|
|
4751
|
+
}
|
|
4752
|
+
async function analyzeServiceErrors(client, serviceName, startTime, endTime, limit = 1e3) {
|
|
4753
|
+
console.log(chalk9.gray(`
|
|
4754
|
+
Fetching traces for service: ${serviceName}...`));
|
|
4755
|
+
const response = await client.fetchTraces({
|
|
4756
|
+
serviceName,
|
|
4757
|
+
startTime,
|
|
4758
|
+
endTime,
|
|
4759
|
+
size: limit
|
|
4760
|
+
});
|
|
4761
|
+
const spans = response.spans || [];
|
|
4762
|
+
console.log(chalk9.gray(` Found ${spans.length} spans`));
|
|
4763
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
4764
|
+
for (const span of spans) {
|
|
4765
|
+
if (!traceMap.has(span.traceId)) {
|
|
4766
|
+
traceMap.set(span.traceId, []);
|
|
4767
|
+
}
|
|
4768
|
+
traceMap.get(span.traceId).push(span);
|
|
4769
|
+
}
|
|
4770
|
+
const totalTraces = traceMap.size;
|
|
4771
|
+
let tracesWithErrors = 0;
|
|
4772
|
+
for (const traceSpans of traceMap.values()) {
|
|
4773
|
+
if (traceSpans.some((s) => s.status === "ERROR")) {
|
|
4774
|
+
tracesWithErrors++;
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
const errorRate = totalTraces > 0 ? tracesWithErrors / totalTraces * 100 : 0;
|
|
4778
|
+
const { errorSpans, patterns, avgDurationMs } = analyzeErrorPatterns(spans);
|
|
4779
|
+
return {
|
|
4780
|
+
serviceName,
|
|
4781
|
+
totalTraces,
|
|
4782
|
+
tracesWithErrors,
|
|
4783
|
+
errorRate,
|
|
4784
|
+
totalErrorSpans: errorSpans.length,
|
|
4785
|
+
errorPatterns: patterns,
|
|
4786
|
+
avgErrorDurationMs: avgDurationMs
|
|
4787
|
+
};
|
|
4788
|
+
}
|
|
4789
|
+
function printServiceAnalysis(analysis) {
|
|
4790
|
+
console.log(chalk9.bold.cyan(`
|
|
4791
|
+
${"=".repeat(60)}`));
|
|
4792
|
+
console.log(chalk9.bold.cyan(`Service: ${analysis.serviceName}`));
|
|
4793
|
+
console.log(chalk9.bold.cyan("=".repeat(60)));
|
|
4794
|
+
console.log(chalk9.white(`Total Traces: ${analysis.totalTraces}`));
|
|
4795
|
+
console.log(chalk9.white(`Traces with Errors: ${analysis.tracesWithErrors}`));
|
|
4796
|
+
const errorRateColor = analysis.errorRate > 10 ? chalk9.red : analysis.errorRate > 5 ? chalk9.yellow : chalk9.green;
|
|
4797
|
+
console.log(errorRateColor(`Error Rate: ${analysis.errorRate.toFixed(2)}%`));
|
|
4798
|
+
console.log(chalk9.white(`Total Error Spans: ${analysis.totalErrorSpans}`));
|
|
4799
|
+
console.log(chalk9.white(`Avg Error Span Duration: ${analysis.avgErrorDurationMs.toFixed(2)}ms`));
|
|
4800
|
+
if (analysis.errorPatterns.length > 0) {
|
|
4801
|
+
console.log(chalk9.bold.white("\nError Patterns:"));
|
|
4802
|
+
for (const pattern of analysis.errorPatterns) {
|
|
4803
|
+
console.log(chalk9.yellow(`
|
|
4804
|
+
\u2022 ${pattern.errorType}`));
|
|
4805
|
+
console.log(chalk9.gray(` Count: ${pattern.count}`));
|
|
4806
|
+
console.log(chalk9.gray(` Avg Duration: ${pattern.avgDurationMs.toFixed(2)}ms`));
|
|
4807
|
+
console.log(chalk9.gray(` Affected Spans: ${pattern.spanNames.join(", ")}`));
|
|
4808
|
+
if (pattern.exampleMessages.length > 0) {
|
|
4809
|
+
console.log(chalk9.gray(` Example Messages:`));
|
|
4810
|
+
pattern.exampleMessages.forEach((msg) => {
|
|
4811
|
+
console.log(chalk9.gray(` - ${msg.substring(0, 80)}${msg.length > 80 ? "..." : ""}`));
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
} else {
|
|
4816
|
+
console.log(chalk9.green("\n\u2713 No error patterns detected"));
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
function printComparison(service1, service2) {
|
|
4820
|
+
console.log(chalk9.bold.magenta(`
|
|
4821
|
+
${"=".repeat(60)}`));
|
|
4822
|
+
console.log(chalk9.bold.magenta("COMPARISON SUMMARY"));
|
|
4823
|
+
console.log(chalk9.bold.magenta("=".repeat(60)));
|
|
4824
|
+
const errorRateDiff = service1.errorRate - service2.errorRate;
|
|
4825
|
+
const diffColor = Math.abs(errorRateDiff) < 1 ? chalk9.white : errorRateDiff > 0 ? chalk9.red : chalk9.green;
|
|
4826
|
+
const diffSymbol = errorRateDiff > 0 ? "\u2191" : errorRateDiff < 0 ? "\u2193" : "=";
|
|
4827
|
+
console.log(chalk9.bold.white("\nError Rate:"));
|
|
4828
|
+
console.log(` ${service1.serviceName}: ${service1.errorRate.toFixed(2)}%`);
|
|
4829
|
+
console.log(` ${service2.serviceName}: ${service2.errorRate.toFixed(2)}%`);
|
|
4830
|
+
console.log(diffColor(` Difference: ${diffSymbol} ${Math.abs(errorRateDiff).toFixed(2)}%`));
|
|
4831
|
+
console.log(chalk9.bold.white("\nUnique Error Patterns:"));
|
|
4832
|
+
const patterns1 = new Set(service1.errorPatterns.map((p) => p.errorType));
|
|
4833
|
+
const patterns2 = new Set(service2.errorPatterns.map((p) => p.errorType));
|
|
4834
|
+
const onlyIn1 = Array.from(patterns1).filter((p) => !patterns2.has(p));
|
|
4835
|
+
const onlyIn2 = Array.from(patterns2).filter((p) => !patterns1.has(p));
|
|
4836
|
+
const inBoth = Array.from(patterns1).filter((p) => patterns2.has(p));
|
|
4837
|
+
if (onlyIn1.length > 0) {
|
|
4838
|
+
console.log(chalk9.cyan(`
|
|
4839
|
+
Only in ${service1.serviceName}:`));
|
|
4840
|
+
onlyIn1.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4841
|
+
}
|
|
4842
|
+
if (onlyIn2.length > 0) {
|
|
4843
|
+
console.log(chalk9.cyan(`
|
|
4844
|
+
Only in ${service2.serviceName}:`));
|
|
4845
|
+
onlyIn2.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4846
|
+
}
|
|
4847
|
+
if (inBoth.length > 0) {
|
|
4848
|
+
console.log(chalk9.cyan(`
|
|
4849
|
+
Common error patterns:`));
|
|
4850
|
+
inBoth.forEach((p) => {
|
|
4851
|
+
const count1 = service1.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4852
|
+
const count2 = service2.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4853
|
+
console.log(chalk9.gray(` \u2022 ${p}: ${count1} vs ${count2}`));
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
console.log(chalk9.bold.yellow("\nRecommendations:"));
|
|
4857
|
+
if (service1.errorRate > service2.errorRate * 1.5) {
|
|
4858
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4859
|
+
} else if (service2.errorRate > service1.errorRate * 1.5) {
|
|
4860
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4861
|
+
} else {
|
|
4862
|
+
console.log(chalk9.green(` \u2713 Error rates are comparable`));
|
|
4863
|
+
}
|
|
4864
|
+
if (onlyIn1.length > 2) {
|
|
4865
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has ${onlyIn1.length} unique error types - review configuration`));
|
|
4866
|
+
}
|
|
4867
|
+
if (onlyIn2.length > 2) {
|
|
4868
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has ${onlyIn2.length} unique error types - review configuration`));
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
function createCompareServicesCommand() {
|
|
4872
|
+
const cmd = new Command9("compare-services");
|
|
4873
|
+
cmd.description("Compare error patterns between two services from trace data").requiredOption("-s, --services <service1,service2>", 'Comma-separated service names (e.g., "lambda-api,eks-api")').option("--start <time>", 'Start time (ISO 8601 format or relative like "1h", "24h")').option("--end <time>", "End time (ISO 8601 format)").option("--limit <number>", "Maximum number of spans to fetch per service", "1000").action(async (options) => {
|
|
4874
|
+
try {
|
|
4875
|
+
const config = await loadConfig();
|
|
4876
|
+
const serverResult = await ensureServer(config.server);
|
|
4877
|
+
const client = new ApiClient(serverResult.baseUrl);
|
|
4878
|
+
const serviceNames = options.services.split(",").map((s) => s.trim());
|
|
4879
|
+
if (serviceNames.length !== 2) {
|
|
4880
|
+
console.error(chalk9.red("Error: Please provide exactly two service names"));
|
|
4881
|
+
process.exit(1);
|
|
4882
|
+
}
|
|
4883
|
+
const [service1Name, service2Name] = serviceNames;
|
|
4884
|
+
const limit = parseInt(options.limit, 10);
|
|
4885
|
+
console.log(chalk9.bold.cyan("\nComparing Error Patterns Between Services"));
|
|
4886
|
+
console.log(chalk9.gray(`Service 1: ${service1Name}`));
|
|
4887
|
+
console.log(chalk9.gray(`Service 2: ${service2Name}`));
|
|
4888
|
+
if (options.start) console.log(chalk9.gray(`Time Range: ${options.start} to ${options.end || "now"}`));
|
|
4889
|
+
const analysis1 = await analyzeServiceErrors(client, service1Name, options.start, options.end, limit);
|
|
4890
|
+
const analysis2 = await analyzeServiceErrors(client, service2Name, options.start, options.end, limit);
|
|
4891
|
+
printServiceAnalysis(analysis1);
|
|
4892
|
+
printServiceAnalysis(analysis2);
|
|
4893
|
+
printComparison(analysis1, analysis2);
|
|
4894
|
+
console.log();
|
|
4895
|
+
} catch (error) {
|
|
4896
|
+
console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4897
|
+
process.exit(1);
|
|
4898
|
+
}
|
|
4899
|
+
});
|
|
4900
|
+
return cmd;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
// cli/commands/remote.ts
|
|
4904
|
+
import { Command as Command10 } from "commander";
|
|
4905
|
+
import chalk10 from "chalk";
|
|
4906
|
+
import fs2 from "fs";
|
|
4907
|
+
import path2 from "path";
|
|
4908
|
+
var CONFIG_FILENAME2 = "agent-health.config.json";
|
|
4909
|
+
function getConfigPath() {
|
|
4910
|
+
return path2.join(process.cwd(), CONFIG_FILENAME2);
|
|
4911
|
+
}
|
|
4912
|
+
function readConfig() {
|
|
4913
|
+
const filePath = getConfigPath();
|
|
4914
|
+
if (!fs2.existsSync(filePath)) return {};
|
|
4915
|
+
try {
|
|
4916
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
4917
|
+
} catch {
|
|
4918
|
+
return {};
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
function writeConfig(config) {
|
|
4922
|
+
fs2.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4923
|
+
}
|
|
4924
|
+
function getRemoteServers(config) {
|
|
4925
|
+
return Array.isArray(config.remoteServers) ? config.remoteServers : [];
|
|
4926
|
+
}
|
|
4927
|
+
function createRemoteCommand() {
|
|
4928
|
+
const remote = new Command10("remote").description("Manage remote agent-health server connections");
|
|
4929
|
+
remote.command("add").description("Add a remote server").requiredOption("--name <name>", "Display name for the server").requiredOption("--url <url>", "Server URL (e.g. http://10.0.1.50:4001)").option("--api-key <key>", "API key for authentication").action((options) => {
|
|
4930
|
+
const config = readConfig();
|
|
4931
|
+
const servers = getRemoteServers(config);
|
|
4932
|
+
if (servers.some((s) => s.name === options.name)) {
|
|
4933
|
+
console.error(chalk10.red(`
|
|
4934
|
+
Error: Server "${options.name}" already exists. Use 'remote remove' first.
|
|
4935
|
+
`));
|
|
4936
|
+
process.exit(1);
|
|
4937
|
+
}
|
|
4938
|
+
const server = { name: options.name, url: options.url.replace(/\/$/, "") };
|
|
4939
|
+
if (options.apiKey) server.apiKey = options.apiKey;
|
|
4940
|
+
servers.push(server);
|
|
4941
|
+
config.remoteServers = servers;
|
|
4942
|
+
writeConfig(config);
|
|
4943
|
+
console.log(chalk10.green(`
|
|
4944
|
+
Added remote server: ${options.name} (${options.url})
|
|
4945
|
+
`));
|
|
4946
|
+
});
|
|
4947
|
+
remote.command("remove").description("Remove a remote server").argument("<name>", "Server name to remove").action((name) => {
|
|
4948
|
+
const config = readConfig();
|
|
4949
|
+
const servers = getRemoteServers(config);
|
|
4950
|
+
const idx = servers.findIndex((s) => s.name === name);
|
|
4951
|
+
if (idx === -1) {
|
|
4952
|
+
console.error(chalk10.red(`
|
|
4953
|
+
Error: Server "${name}" not found.
|
|
4954
|
+
`));
|
|
4955
|
+
process.exit(1);
|
|
4956
|
+
}
|
|
4957
|
+
servers.splice(idx, 1);
|
|
4958
|
+
config.remoteServers = servers;
|
|
4959
|
+
writeConfig(config);
|
|
4960
|
+
console.log(chalk10.green(`
|
|
4961
|
+
Removed remote server: ${name}
|
|
4962
|
+
`));
|
|
4963
|
+
});
|
|
4964
|
+
remote.command("list").description("List configured remote servers").action(() => {
|
|
4965
|
+
const config = readConfig();
|
|
4966
|
+
const servers = getRemoteServers(config);
|
|
4967
|
+
if (servers.length === 0) {
|
|
4968
|
+
console.log(chalk10.gray("\n No remote servers configured.\n"));
|
|
4969
|
+
console.log(chalk10.gray(" Add one with: agent-health remote add --name <name> --url <url>\n"));
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4972
|
+
console.log(chalk10.cyan(`
|
|
4973
|
+
Remote Servers (${servers.length}):
|
|
4974
|
+
`));
|
|
4975
|
+
for (const s of servers) {
|
|
4976
|
+
const auth = s.apiKey ? chalk10.green(" [auth]") : chalk10.gray(" [no auth]");
|
|
4977
|
+
console.log(` ${chalk10.bold(s.name)} ${s.url}${auth}`);
|
|
4978
|
+
}
|
|
4979
|
+
console.log("");
|
|
4980
|
+
});
|
|
4981
|
+
remote.command("test").description("Test connectivity to all remote servers").action(async () => {
|
|
4982
|
+
const config = readConfig();
|
|
4983
|
+
const servers = getRemoteServers(config);
|
|
4984
|
+
if (servers.length === 0) {
|
|
4985
|
+
console.log(chalk10.gray("\n No remote servers configured.\n"));
|
|
4986
|
+
return;
|
|
4987
|
+
}
|
|
4988
|
+
console.log(chalk10.cyan(`
|
|
4989
|
+
Testing ${servers.length} remote server(s)...
|
|
4990
|
+
`));
|
|
4991
|
+
for (const s of servers) {
|
|
4992
|
+
try {
|
|
4993
|
+
const headers = {};
|
|
4994
|
+
if (s.apiKey) headers["Authorization"] = `Bearer ${s.apiKey}`;
|
|
4995
|
+
const controller = new AbortController();
|
|
4996
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
4997
|
+
const response = await fetch(`${s.url}/api/coding-agents/available`, {
|
|
4998
|
+
headers,
|
|
4999
|
+
signal: controller.signal
|
|
5000
|
+
});
|
|
5001
|
+
clearTimeout(timer);
|
|
5002
|
+
if (response.ok) {
|
|
5003
|
+
const data = await response.json();
|
|
5004
|
+
const agentCount = data.agents?.length ?? 0;
|
|
5005
|
+
console.log(chalk10.green(` \u2713 ${s.name} \u2014 OK (${agentCount} agents detected)`));
|
|
5006
|
+
} else {
|
|
5007
|
+
console.log(chalk10.red(` \u2717 ${s.name} \u2014 HTTP ${response.status} ${response.statusText}`));
|
|
5008
|
+
}
|
|
5009
|
+
} catch (error) {
|
|
5010
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5011
|
+
console.log(chalk10.red(` \u2717 ${s.name} \u2014 ${msg}`));
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
console.log("");
|
|
5015
|
+
});
|
|
5016
|
+
return remote;
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4429
5019
|
// cli/index.ts
|
|
4430
5020
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
4431
5021
|
var __dirname3 = dirname3(__filename3);
|
|
@@ -4439,59 +5029,126 @@ try {
|
|
|
4439
5029
|
function loadEnvFile(envPath) {
|
|
4440
5030
|
const absolutePath = resolve4(process.cwd(), envPath);
|
|
4441
5031
|
if (!existsSync5(absolutePath)) {
|
|
4442
|
-
console.error(
|
|
5032
|
+
console.error(chalk11.red(`
|
|
4443
5033
|
Error: Environment file not found: ${absolutePath}
|
|
4444
5034
|
`));
|
|
4445
5035
|
process.exit(1);
|
|
4446
5036
|
}
|
|
4447
5037
|
const result = loadDotenv({ path: absolutePath });
|
|
4448
5038
|
if (result.error) {
|
|
4449
|
-
console.error(
|
|
5039
|
+
console.error(chalk11.red(`
|
|
4450
5040
|
Error loading environment file: ${result.error.message}
|
|
4451
5041
|
`));
|
|
4452
5042
|
process.exit(1);
|
|
4453
5043
|
}
|
|
4454
|
-
console.log(
|
|
5044
|
+
console.log(chalk11.gray(` Loaded environment from: ${envPath}`));
|
|
4455
5045
|
}
|
|
4456
5046
|
var defaultEnvPath = resolve4(process.cwd(), ".env");
|
|
4457
5047
|
if (existsSync5(defaultEnvPath)) {
|
|
4458
5048
|
loadDotenv({ path: defaultEnvPath });
|
|
4459
5049
|
}
|
|
4460
|
-
var program = new
|
|
4461
|
-
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions()
|
|
4462
|
-
|
|
5050
|
+
var program = new Command11();
|
|
5051
|
+
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
|
|
5052
|
+
sortSubcommands: false,
|
|
5053
|
+
// Hide default command list — replaced by grouped custom help below
|
|
5054
|
+
subcommandTerm: () => "",
|
|
5055
|
+
formatHelp: (cmd, helper) => {
|
|
5056
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
5057
|
+
const helpWidth = helper.helpWidth || 80;
|
|
5058
|
+
const output = [];
|
|
5059
|
+
const desc = helper.commandDescription(cmd);
|
|
5060
|
+
if (desc) {
|
|
5061
|
+
output.push(desc, "");
|
|
5062
|
+
}
|
|
5063
|
+
output.push(`${chalk11.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
|
|
5064
|
+
const optionList = helper.visibleOptions(cmd).map((opt) => {
|
|
5065
|
+
const term = helper.optionTerm(opt);
|
|
5066
|
+
const desc2 = helper.optionDescription(opt);
|
|
5067
|
+
return ` ${term.padEnd(termWidth)} ${desc2}`;
|
|
5068
|
+
}).join("\n");
|
|
5069
|
+
if (optionList) {
|
|
5070
|
+
output.push(`${chalk11.cyan.bold("Options:")}`, optionList, "");
|
|
5071
|
+
}
|
|
5072
|
+
return output.join("\n");
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
program.addHelpText("after", `
|
|
5076
|
+
${chalk11.cyan.bold("Getting Started:")}
|
|
5077
|
+
${chalk11.yellow("agent-health")} Launch the web UI and evaluation server
|
|
5078
|
+
${chalk11.yellow("agent-health init")} Generate an agent-health.config.ts file
|
|
5079
|
+
${chalk11.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
|
|
5080
|
+
|
|
5081
|
+
${chalk11.cyan.bold("Running Evaluations:")}
|
|
5082
|
+
${chalk11.yellow("agent-health run")} ${chalk11.gray("-t <case> -a <agent>")} Run a single test case against an agent
|
|
5083
|
+
${chalk11.yellow("agent-health benchmark")} ${chalk11.gray("-f <file>")} Run a full benchmark from a test cases JSON file
|
|
5084
|
+
${chalk11.yellow("agent-health benchmark")} ${chalk11.gray("-b <id>")} Re-run an existing benchmark
|
|
5085
|
+
|
|
5086
|
+
${chalk11.cyan.bold("Viewing Results:")}
|
|
5087
|
+
${chalk11.yellow("agent-health list")} ${chalk11.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
|
|
5088
|
+
${chalk11.yellow("agent-health report")} ${chalk11.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
|
|
5089
|
+
${chalk11.yellow("agent-health export")} ${chalk11.gray("-b <benchmark>")} Export test cases as re-importable JSON
|
|
5090
|
+
${chalk11.yellow("agent-health compare-services")} ${chalk11.gray("-s A B")} Compare error patterns between services
|
|
5091
|
+
|
|
5092
|
+
${chalk11.cyan.bold("Remote Servers:")}
|
|
5093
|
+
${chalk11.yellow("agent-health remote add")} ${chalk11.gray("--name <n> --url <u>")} Add a remote server
|
|
5094
|
+
${chalk11.yellow("agent-health remote list")} List configured remote servers
|
|
5095
|
+
${chalk11.yellow("agent-health remote test")} Test connectivity to all remotes
|
|
5096
|
+
|
|
5097
|
+
${chalk11.cyan.bold("Maintenance:")}
|
|
5098
|
+
${chalk11.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
|
|
5099
|
+
${chalk11.yellow("agent-health serve")} Start the server (same as default, explicit command)
|
|
5100
|
+
|
|
5101
|
+
${chalk11.cyan.bold("Examples:")}
|
|
5102
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health
|
|
5103
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
|
|
5104
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
|
|
5105
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
|
|
5106
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health list agents
|
|
5107
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
|
|
5108
|
+
${chalk11.gray("$")} npx @opensearch-project/agent-health serve --headless --api-key sk-secret
|
|
5109
|
+
`);
|
|
5110
|
+
program.option("-p, --port <number>", "Server port", "4001").option("-e, --env-file <path>", "Load environment variables from file (e.g., .env)").option("--no-browser", "Do not open browser automatically").option("--headless", "Run API server only (no frontend, no browser)").option("--api-key <key>", "Require API key for coding-agents endpoints");
|
|
4463
5111
|
program.action(async (options) => {
|
|
4464
|
-
console.log(
|
|
5112
|
+
console.log(chalk11.cyan.bold(`
|
|
4465
5113
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4466
5114
|
`));
|
|
4467
|
-
console.log(
|
|
4468
|
-
console.log(
|
|
5115
|
+
console.log(chalk11.gray(` Working directory: ${process.cwd()}`));
|
|
5116
|
+
console.log(chalk11.gray(` Package directory: ${__dirname3}`));
|
|
4469
5117
|
if (options.envFile) {
|
|
4470
5118
|
loadEnvFile(options.envFile);
|
|
4471
5119
|
} else if (existsSync5(defaultEnvPath)) {
|
|
4472
|
-
console.log(
|
|
5120
|
+
console.log(chalk11.gray(" Auto-loaded .env from current directory"));
|
|
4473
5121
|
}
|
|
4474
5122
|
const port = parseInt(options.port, 10);
|
|
4475
|
-
const
|
|
5123
|
+
const headless = options.headless || false;
|
|
5124
|
+
const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
|
|
4476
5125
|
try {
|
|
4477
|
-
await startServer({ port });
|
|
4478
|
-
spinner.succeed("Server started");
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
5126
|
+
await startServer({ port, headless, apiKey: options.apiKey });
|
|
5127
|
+
spinner.succeed(headless ? "Headless API server started" : "Server started");
|
|
5128
|
+
if (headless) {
|
|
5129
|
+
console.log(chalk11.green(`
|
|
5130
|
+
API server running on http://0.0.0.0:${port}`));
|
|
5131
|
+
if (options.apiKey) console.log(chalk11.gray(" API key authentication enabled"));
|
|
5132
|
+
console.log(chalk11.gray(" Mode: headless (API only, no frontend)\n"));
|
|
5133
|
+
} else {
|
|
5134
|
+
console.log(chalk11.gray("\n Configuration:"));
|
|
5135
|
+
console.log(chalk11.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
|
|
5136
|
+
console.log(chalk11.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
|
|
5137
|
+
console.log(chalk11.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
|
|
4483
5138
|
`));
|
|
4484
|
-
|
|
4485
|
-
|
|
5139
|
+
const url = `http://localhost:${port}`;
|
|
5140
|
+
console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
|
|
4486
5141
|
`));
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
5142
|
+
console.log(chalk11.green(` Demo data loaded`));
|
|
5143
|
+
if (options.browser !== false) {
|
|
5144
|
+
console.log(chalk11.gray(" Opening browser..."));
|
|
5145
|
+
await open(url);
|
|
5146
|
+
}
|
|
4490
5147
|
}
|
|
4491
|
-
console.log(
|
|
5148
|
+
console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
|
|
4492
5149
|
} catch (error) {
|
|
4493
5150
|
spinner.fail("Failed to start server");
|
|
4494
|
-
console.error(
|
|
5151
|
+
console.error(chalk11.red(`
|
|
4495
5152
|
Error: ${error instanceof Error ? error.message : error}
|
|
4496
5153
|
`));
|
|
4497
5154
|
process.exit(1);
|
|
@@ -4505,26 +5162,35 @@ program.addCommand(createReportCommand());
|
|
|
4505
5162
|
program.addCommand(createDoctorCommand());
|
|
4506
5163
|
program.addCommand(createInitCommand());
|
|
4507
5164
|
program.addCommand(createMigrateCommand());
|
|
4508
|
-
program.
|
|
4509
|
-
|
|
5165
|
+
program.addCommand(createCompareServicesCommand());
|
|
5166
|
+
program.addCommand(createRemoteCommand());
|
|
5167
|
+
program.command("serve").description("Start the Agent Health server (same as default action)").option("-p, --port <number>", "Server port", "4001").option("--no-browser", "Do not open browser automatically").option("--headless", "Run API server only (no frontend, no browser)").option("--api-key <key>", "Require API key for coding-agents endpoints").action(async (options) => {
|
|
5168
|
+
console.log(chalk11.cyan.bold(`
|
|
4510
5169
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4511
5170
|
`));
|
|
4512
5171
|
const port = parseInt(options.port, 10);
|
|
4513
|
-
const
|
|
5172
|
+
const headless = options.headless || false;
|
|
5173
|
+
const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
|
|
4514
5174
|
try {
|
|
4515
|
-
await startServer({ port });
|
|
4516
|
-
spinner.succeed("Server started");
|
|
5175
|
+
await startServer({ port, headless, apiKey: options.apiKey });
|
|
5176
|
+
spinner.succeed(headless ? "Headless API server started" : "Server started");
|
|
4517
5177
|
const url = `http://localhost:${port}`;
|
|
4518
|
-
|
|
5178
|
+
if (headless) {
|
|
5179
|
+
console.log(chalk11.green(` API server running on http://0.0.0.0:${port}`));
|
|
5180
|
+
if (options.apiKey) console.log(chalk11.gray(" API key authentication enabled"));
|
|
5181
|
+
console.log(chalk11.gray(" Mode: headless (API only, no frontend)\n"));
|
|
5182
|
+
} else {
|
|
5183
|
+
console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
|
|
4519
5184
|
`));
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
5185
|
+
if (options.browser !== false) {
|
|
5186
|
+
console.log(chalk11.gray(" Opening browser..."));
|
|
5187
|
+
await open(url);
|
|
5188
|
+
}
|
|
4523
5189
|
}
|
|
4524
|
-
console.log(
|
|
5190
|
+
console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
|
|
4525
5191
|
} catch (error) {
|
|
4526
5192
|
spinner.fail("Failed to start server");
|
|
4527
|
-
console.error(
|
|
5193
|
+
console.error(chalk11.red(`
|
|
4528
5194
|
Error: ${error instanceof Error ? error.message : error}
|
|
4529
5195
|
`));
|
|
4530
5196
|
process.exit(1);
|
|
@@ -4533,15 +5199,15 @@ program.command("serve").description("Start the Agent Health server (same as def
|
|
|
4533
5199
|
program.on("command:*", (operands) => {
|
|
4534
5200
|
const unknownCommand = operands[0];
|
|
4535
5201
|
const availableCommands = program.commands.map((cmd) => cmd.name());
|
|
4536
|
-
console.error(
|
|
5202
|
+
console.error(chalk11.red(`
|
|
4537
5203
|
Error: Unknown command '${unknownCommand}'`));
|
|
4538
5204
|
console.log("");
|
|
4539
|
-
console.log(
|
|
5205
|
+
console.log(chalk11.cyan(" Available commands:"));
|
|
4540
5206
|
for (const cmd of availableCommands) {
|
|
4541
|
-
console.log(
|
|
5207
|
+
console.log(chalk11.gray(` - ${cmd}`));
|
|
4542
5208
|
}
|
|
4543
5209
|
console.log("");
|
|
4544
|
-
console.log(
|
|
5210
|
+
console.log(chalk11.gray(` Run ${chalk11.cyan("agent-health --help")} for usage information.
|
|
4545
5211
|
`));
|
|
4546
5212
|
process.exitCode = 1;
|
|
4547
5213
|
});
|