@kortyx/cli 0.11.4 → 0.13.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/CHANGELOG.md +38 -0
- package/README.md +132 -0
- package/dist/index.js +1382 -265
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -23,15 +23,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/index.ts
|
|
26
|
-
var
|
|
26
|
+
var import_promises5 = require("fs/promises");
|
|
27
27
|
var import_node_module = __toESM(require("module"));
|
|
28
28
|
var import_node_os3 = require("os");
|
|
29
|
-
var
|
|
30
|
-
var
|
|
29
|
+
var import_node_path6 = require("path");
|
|
30
|
+
var import_agent2 = require("@kortyx/agent");
|
|
31
31
|
var import_errors = require("@kortyx/core/errors");
|
|
32
32
|
var import_telemetry_contracts6 = require("@kortyx/telemetry-contracts");
|
|
33
|
-
var
|
|
33
|
+
var import_commander6 = require("commander");
|
|
34
34
|
var import_api = require("tsx/cjs/api");
|
|
35
|
+
var import_api2 = require("tsx/esm/api");
|
|
36
|
+
var import_typescript2 = __toESM(require("typescript"));
|
|
35
37
|
|
|
36
38
|
// src/connections-command.ts
|
|
37
39
|
var import_telemetry_contracts2 = require("@kortyx/telemetry-contracts");
|
|
@@ -87,7 +89,7 @@ var normalizeConnectionUrl = (input) => {
|
|
|
87
89
|
}
|
|
88
90
|
return url.toString().replace(/\/+$/, "");
|
|
89
91
|
};
|
|
90
|
-
var
|
|
92
|
+
var StudioApiTransport = class {
|
|
91
93
|
constructor(apiUrl, apiKey, request = fetch) {
|
|
92
94
|
this.apiKey = apiKey;
|
|
93
95
|
this.request = request;
|
|
@@ -106,27 +108,21 @@ var StudioReadClient = class {
|
|
|
106
108
|
protocolVersion: null,
|
|
107
109
|
release: null
|
|
108
110
|
};
|
|
109
|
-
async
|
|
110
|
-
if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?)$/.test(
|
|
111
|
-
path
|
|
112
|
-
) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
|
|
113
|
-
throw new StudioReadError(
|
|
114
|
-
"invalid_endpoint",
|
|
115
|
-
"Only supported Studio read endpoints are allowed."
|
|
116
|
-
);
|
|
117
|
-
}
|
|
111
|
+
async requestJson(method, path, schema, query = {}, body) {
|
|
118
112
|
const url = new URL(`${this.apiUrl}${path}`);
|
|
119
113
|
for (const [key, value2] of Object.entries(query))
|
|
120
114
|
url.searchParams.set(key, value2);
|
|
121
115
|
let response;
|
|
122
116
|
try {
|
|
123
117
|
response = await this.request(url, {
|
|
124
|
-
method
|
|
118
|
+
method,
|
|
125
119
|
headers: {
|
|
126
120
|
authorization: `Bearer ${this.apiKey}`,
|
|
127
121
|
accept: "application/json",
|
|
128
|
-
"x-kortyx-studio-api-version": import_telemetry_contracts.STUDIO_API_PROTOCOL_VERSION
|
|
122
|
+
"x-kortyx-studio-api-version": import_telemetry_contracts.STUDIO_API_PROTOCOL_VERSION,
|
|
123
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
129
124
|
},
|
|
125
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
130
126
|
redirect: "error",
|
|
131
127
|
signal: AbortSignal.timeout(15e3)
|
|
132
128
|
});
|
|
@@ -152,7 +148,7 @@ var StudioReadClient = class {
|
|
|
152
148
|
}
|
|
153
149
|
if (!response.ok) {
|
|
154
150
|
await response.body?.cancel();
|
|
155
|
-
const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read permission." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
|
|
151
|
+
const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read or the operation's required permission (eval:run for execution)." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
|
|
156
152
|
throw new StudioReadError(
|
|
157
153
|
"api_error",
|
|
158
154
|
message,
|
|
@@ -169,7 +165,7 @@ var StudioReadClient = class {
|
|
|
169
165
|
);
|
|
170
166
|
try {
|
|
171
167
|
const decoder = new TextDecoder();
|
|
172
|
-
let
|
|
168
|
+
let body2 = "";
|
|
173
169
|
let bytes = 0;
|
|
174
170
|
for (; ; ) {
|
|
175
171
|
const chunk = await reader.read();
|
|
@@ -182,10 +178,10 @@ var StudioReadClient = class {
|
|
|
182
178
|
"Studio response exceeded the 20 MiB safety limit. Use a smaller query or inspect an individual run."
|
|
183
179
|
);
|
|
184
180
|
}
|
|
185
|
-
|
|
181
|
+
body2 += decoder.decode(chunk.value, { stream: true });
|
|
186
182
|
}
|
|
187
|
-
|
|
188
|
-
value = JSON.parse(
|
|
183
|
+
body2 += decoder.decode();
|
|
184
|
+
value = JSON.parse(body2);
|
|
189
185
|
} catch (error) {
|
|
190
186
|
if (error instanceof StudioReadError) throw error;
|
|
191
187
|
throw new StudioReadError(
|
|
@@ -210,6 +206,19 @@ var StudioReadClient = class {
|
|
|
210
206
|
return result.data;
|
|
211
207
|
}
|
|
212
208
|
};
|
|
209
|
+
var StudioReadClient = class extends StudioApiTransport {
|
|
210
|
+
async get(path, schema, query = {}) {
|
|
211
|
+
if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?)$/.test(
|
|
212
|
+
path
|
|
213
|
+
) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
|
|
214
|
+
throw new StudioReadError(
|
|
215
|
+
"invalid_endpoint",
|
|
216
|
+
"Only supported Studio read endpoints are allowed."
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return this.requestJson("GET", path, schema, query);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
213
222
|
|
|
214
223
|
// src/studio/state.ts
|
|
215
224
|
var import_promises = require("fs/promises");
|
|
@@ -227,6 +236,7 @@ x-api-env: &api-env
|
|
|
227
236
|
x-bootstrap-keys: &bootstrap-keys
|
|
228
237
|
KORTYX_TELEMETRY_API_KEY: \${KORTYX_TELEMETRY_API_KEY}
|
|
229
238
|
KORTYX_STUDIO_API_KEY: \${KORTYX_STUDIO_API_KEY}
|
|
239
|
+
KORTYX_STUDIO_ENABLE_EVALS: \${KORTYX_STUDIO_ENABLE_EVALS:-0}
|
|
230
240
|
KORTYX_STUDIO_ENABLE_REVIEWS: \${KORTYX_STUDIO_ENABLE_REVIEWS:-0}
|
|
231
241
|
|
|
232
242
|
services:
|
|
@@ -247,6 +257,7 @@ services:
|
|
|
247
257
|
db-init:
|
|
248
258
|
image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
|
|
249
259
|
pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
|
|
260
|
+
user: "1000:1000"
|
|
250
261
|
environment:
|
|
251
262
|
<<: [*api-env, *bootstrap-keys]
|
|
252
263
|
command: >
|
|
@@ -260,11 +271,18 @@ services:
|
|
|
260
271
|
api:
|
|
261
272
|
image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
|
|
262
273
|
pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
|
|
274
|
+
user: "1000:1000"
|
|
263
275
|
environment:
|
|
264
276
|
<<: *api-env
|
|
265
277
|
NODE_ENV: production
|
|
266
278
|
API_HOST: 0.0.0.0
|
|
267
279
|
API_PORT: 6400
|
|
280
|
+
KORTYX_EVAL_JUDGE_MODEL: \${KORTYX_EVAL_JUDGE_MODEL:-}
|
|
281
|
+
KORTYX_EVAL_JUDGE_API_KEY: \${KORTYX_EVAL_JUDGE_API_KEY:-}
|
|
282
|
+
KORTYX_EVAL_JUDGE_API: \${KORTYX_EVAL_JUDGE_API:-responses}
|
|
283
|
+
KORTYX_EVAL_JUDGE_BASE_URL: \${KORTYX_EVAL_JUDGE_BASE_URL:-}
|
|
284
|
+
KORTYX_EVAL_JUDGE_ID: \${KORTYX_EVAL_JUDGE_ID:-}
|
|
285
|
+
KORTYX_EVAL_JUDGE_VERSION: \${KORTYX_EVAL_JUDGE_VERSION:-}
|
|
268
286
|
KORTYX_STUDIO_RELEASE: \${KORTYX_STUDIO_IMAGE_TAG:-latest}
|
|
269
287
|
ports:
|
|
270
288
|
- "127.0.0.1:\${API_PORT:-6400}:6400"
|
|
@@ -313,6 +331,9 @@ services:
|
|
|
313
331
|
image: \${KORTYX_API_IMAGE_REF:-\${KORTYX_API_IMAGE:-ghcr.io/kortyx-io/kortyx-api}:\${KORTYX_STUDIO_IMAGE_TAG:-latest}}
|
|
314
332
|
pull_policy: \${KORTYX_STUDIO_PULL_POLICY:-always}
|
|
315
333
|
restart: unless-stopped
|
|
334
|
+
# The updater alone needs root for the host Docker socket and ownership-safe
|
|
335
|
+
# writes to the bind-mounted Studio state directory.
|
|
336
|
+
user: "0:0"
|
|
316
337
|
command: ["node", "apps/api/dist/updater.js", "serve", "\${KORTYX_STUDIO_STATE_DIR}"]
|
|
317
338
|
volumes:
|
|
318
339
|
- type: bind
|
|
@@ -816,7 +837,7 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
|
|
|
816
837
|
"/v1/studio/context",
|
|
817
838
|
import_telemetry_contracts2.StudioContextResponseSchema
|
|
818
839
|
);
|
|
819
|
-
if (!context.apiKey
|
|
840
|
+
if (!context.apiKey?.scopes.includes("studio:read"))
|
|
820
841
|
throw new StudioReadError(
|
|
821
842
|
"missing_scope",
|
|
822
843
|
"The API key lacks studio:read permission."
|
|
@@ -837,49 +858,1215 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
|
|
|
837
858
|
await saveConnections(config, options.configHome);
|
|
838
859
|
print({ connection: profile, verified: true }, options);
|
|
839
860
|
}
|
|
840
|
-
);
|
|
841
|
-
common(
|
|
842
|
-
command.command("use <name>").description(
|
|
843
|
-
"Select the default connection; agents should prefer per-command --connection."
|
|
844
|
-
)
|
|
845
|
-
).action(async (name, options) => {
|
|
846
|
-
const config = await readConnections(options.configHome);
|
|
847
|
-
if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
|
|
861
|
+
);
|
|
862
|
+
common(
|
|
863
|
+
command.command("use <name>").description(
|
|
864
|
+
"Select the default connection; agents should prefer per-command --connection."
|
|
865
|
+
)
|
|
866
|
+
).action(async (name, options) => {
|
|
867
|
+
const config = await readConnections(options.configHome);
|
|
868
|
+
if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
|
|
869
|
+
throw new StudioReadError(
|
|
870
|
+
"unknown_connection",
|
|
871
|
+
"Connection not found. Run kortyx connections list."
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
config.current = name;
|
|
875
|
+
await saveConnections(config, options.configHome);
|
|
876
|
+
print({ current: name }, options);
|
|
877
|
+
});
|
|
878
|
+
common(
|
|
879
|
+
command.command("remove <name>").description(
|
|
880
|
+
"Remove a saved locator, not its credential or remote project."
|
|
881
|
+
)
|
|
882
|
+
).action(async (name, options) => {
|
|
883
|
+
const config = await readConnections(options.configHome);
|
|
884
|
+
if (!config.profiles.some((profile) => profile.name === name))
|
|
885
|
+
throw new StudioReadError(
|
|
886
|
+
"unknown_connection",
|
|
887
|
+
"Saved connection not found; managed local cannot be removed."
|
|
888
|
+
);
|
|
889
|
+
config.profiles = config.profiles.filter(
|
|
890
|
+
(profile) => profile.name !== name
|
|
891
|
+
);
|
|
892
|
+
if (config.current === name) delete config.current;
|
|
893
|
+
await saveConnections(config, options.configHome);
|
|
894
|
+
print({ removed: name }, options);
|
|
895
|
+
});
|
|
896
|
+
return command;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/evals/command.ts
|
|
900
|
+
var import_node_path3 = require("path");
|
|
901
|
+
var import_agent = require("@kortyx/agent");
|
|
902
|
+
var import_commander2 = require("commander");
|
|
903
|
+
|
|
904
|
+
// src/evals/reporter.ts
|
|
905
|
+
var text = (value) => value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim();
|
|
906
|
+
var duration = (ms) => ms < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(2)} s`;
|
|
907
|
+
var wide = /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u;
|
|
908
|
+
var fit = (value, columns) => {
|
|
909
|
+
const clean = value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trimEnd();
|
|
910
|
+
let used = 0;
|
|
911
|
+
let result = "";
|
|
912
|
+
for (const char of clean) {
|
|
913
|
+
used += /\p{Mark}/u.test(char) ? 0 : wide.test(char) ? 2 : 1;
|
|
914
|
+
if (used > columns - 2) return `${result}\u2026`;
|
|
915
|
+
result += char;
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
};
|
|
919
|
+
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
920
|
+
var phaseNames = {
|
|
921
|
+
params: "Preparing inputs",
|
|
922
|
+
setup: "Setting up test",
|
|
923
|
+
execute: "Running workflow",
|
|
924
|
+
responder: "Resolving human input",
|
|
925
|
+
reference: "Loading reference",
|
|
926
|
+
grading: "Checking expectations",
|
|
927
|
+
cleanup: "Cleaning up",
|
|
928
|
+
reporting: "Reporting results"
|
|
929
|
+
};
|
|
930
|
+
function createEvalTerminalReporter(write, color, options = {}) {
|
|
931
|
+
const now = options.now ?? Date.now;
|
|
932
|
+
const columns = () => Math.max(12, options.columns?.() ?? process.stdout.columns ?? 100);
|
|
933
|
+
const paint = (code, value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
934
|
+
const active = /* @__PURE__ */ new Map();
|
|
935
|
+
let names = /* @__PURE__ */ new Map();
|
|
936
|
+
let steps = /* @__PURE__ */ new Map();
|
|
937
|
+
let completed = 0;
|
|
938
|
+
let total = 0;
|
|
939
|
+
let started = 0;
|
|
940
|
+
let frame = 0;
|
|
941
|
+
let liveLines = 0;
|
|
942
|
+
let paused = false;
|
|
943
|
+
let running = false;
|
|
944
|
+
let timer;
|
|
945
|
+
const key = (id, repetition) => JSON.stringify([id, repetition]);
|
|
946
|
+
const clear = () => {
|
|
947
|
+
if (liveLines) write(`\x1B[${liveLines}A\r\x1B[0J`);
|
|
948
|
+
liveLines = 0;
|
|
949
|
+
};
|
|
950
|
+
const line = (value = "") => {
|
|
951
|
+
clear();
|
|
952
|
+
write(`${value}
|
|
953
|
+
`);
|
|
954
|
+
};
|
|
955
|
+
const progressBar = (done, count, width, pulse = false) => {
|
|
956
|
+
const filled = count ? Math.min(width, Math.floor(done / count * width)) : 0;
|
|
957
|
+
const remaining = width - filled;
|
|
958
|
+
const pending = pulse && remaining > 0 ? Array.from(
|
|
959
|
+
{ length: remaining },
|
|
960
|
+
(_, index) => index === frame % remaining ? "\u2578" : "\u2591"
|
|
961
|
+
).join("") : "\u2591".repeat(remaining);
|
|
962
|
+
return `${"\u2501".repeat(filled)}${pending}`;
|
|
963
|
+
};
|
|
964
|
+
const render = () => {
|
|
965
|
+
if (!color || !running || paused) return;
|
|
966
|
+
clear();
|
|
967
|
+
const width = columns();
|
|
968
|
+
const barWidth = Math.max(6, Math.min(22, width - 60));
|
|
969
|
+
const percent = total ? Math.floor(completed / total * 100) : 0;
|
|
970
|
+
const lines = [
|
|
971
|
+
paint(
|
|
972
|
+
36,
|
|
973
|
+
fit(
|
|
974
|
+
` RUN ${progressBar(completed, total, barWidth)} ${completed}/${total || "?"} \xB7 ${percent}%`,
|
|
975
|
+
width
|
|
976
|
+
)
|
|
977
|
+
),
|
|
978
|
+
paint(
|
|
979
|
+
90,
|
|
980
|
+
fit(
|
|
981
|
+
` ${active.size} running \xB7 ${Math.max(0, total - completed - active.size)} queued \xB7 ${((now() - started) / 1e3).toFixed(1)}s elapsed`,
|
|
982
|
+
width
|
|
983
|
+
)
|
|
984
|
+
)
|
|
985
|
+
];
|
|
986
|
+
for (const item of [...active.values()].slice(0, 6)) {
|
|
987
|
+
const name = names.get(item.caseId) ?? item.caseId;
|
|
988
|
+
lines.push(
|
|
989
|
+
paint(
|
|
990
|
+
33,
|
|
991
|
+
fit(
|
|
992
|
+
` ${frames[frame % frames.length]} ${name} #${item.repetition}`,
|
|
993
|
+
width
|
|
994
|
+
)
|
|
995
|
+
)
|
|
996
|
+
);
|
|
997
|
+
const stepCount = steps.get(item.caseId) ?? 1;
|
|
998
|
+
const step = item.stepIndex === void 0 ? "" : `Step ${item.stepIndex + 1}/${stepCount} \xB7 `;
|
|
999
|
+
lines.push(
|
|
1000
|
+
paint(
|
|
1001
|
+
item.phase === "grading" ? 35 : 90,
|
|
1002
|
+
fit(
|
|
1003
|
+
` ${progressBar(item.completedSteps, stepCount, 6, true)} ${step}${phaseNames[item.phase]} \xB7 ${((now() - item.started) / 1e3).toFixed(1)}s`,
|
|
1004
|
+
width
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (active.size > 6)
|
|
1010
|
+
lines.push(paint(90, ` +${active.size - 6} more running`));
|
|
1011
|
+
write(`${lines.join("\n")}
|
|
1012
|
+
`);
|
|
1013
|
+
liveLines = lines.length;
|
|
1014
|
+
frame++;
|
|
1015
|
+
};
|
|
1016
|
+
const stop = () => {
|
|
1017
|
+
if (timer) clearInterval(timer);
|
|
1018
|
+
timer = void 0;
|
|
1019
|
+
running = false;
|
|
1020
|
+
clear();
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
start(suite) {
|
|
1024
|
+
stop();
|
|
1025
|
+
names = new Map(
|
|
1026
|
+
suite.cases.map((item) => [item.id, item.name ?? item.id])
|
|
1027
|
+
);
|
|
1028
|
+
steps = new Map(suite.cases.map((item) => [item.id, item.steps.length]));
|
|
1029
|
+
active.clear();
|
|
1030
|
+
completed = total = frame = 0;
|
|
1031
|
+
paused = false;
|
|
1032
|
+
started = now();
|
|
1033
|
+
line();
|
|
1034
|
+
if (color)
|
|
1035
|
+
line(paint(35, " \u2501".padEnd(Math.min(columns() - 1, 64), "\u2501")));
|
|
1036
|
+
line(paint("1;35", `\u25C6 Kortyx Evals \xB7 ${text(suite.name ?? suite.id)}`));
|
|
1037
|
+
line(paint(90, ` ${text(suite.id)} \xB7 local execution`));
|
|
1038
|
+
line();
|
|
1039
|
+
running = true;
|
|
1040
|
+
if (color) {
|
|
1041
|
+
timer = setInterval(render, 100);
|
|
1042
|
+
timer.unref();
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
progress(event) {
|
|
1046
|
+
if (event.type === "run-started") {
|
|
1047
|
+
total = event.caseIds.length * event.repetitions;
|
|
1048
|
+
line(
|
|
1049
|
+
paint(
|
|
1050
|
+
90,
|
|
1051
|
+
` ${event.caseIds.length} ${event.caseIds.length === 1 ? "test" : "tests"} \xB7 ${total} ${total === 1 ? "attempt" : "attempts"} \xB7 concurrency ${event.concurrency}`
|
|
1052
|
+
)
|
|
1053
|
+
);
|
|
1054
|
+
line();
|
|
1055
|
+
} else if (event.type === "case-started") {
|
|
1056
|
+
active.set(key(event.caseId, event.repetition), {
|
|
1057
|
+
caseId: event.caseId,
|
|
1058
|
+
repetition: event.repetition,
|
|
1059
|
+
started: now(),
|
|
1060
|
+
phase: "params",
|
|
1061
|
+
completedSteps: 0
|
|
1062
|
+
});
|
|
1063
|
+
if (!color)
|
|
1064
|
+
line(
|
|
1065
|
+
` \u2192 ${text(names.get(event.caseId) ?? event.caseId)} [${text(event.caseId)}] #${event.repetition} RUNNING`
|
|
1066
|
+
);
|
|
1067
|
+
} else if (event.type === "case-progress") {
|
|
1068
|
+
const item = active.get(key(event.caseId, event.repetition));
|
|
1069
|
+
if (item) {
|
|
1070
|
+
item.phase = event.phase;
|
|
1071
|
+
item.stepIndex = event.stepIndex;
|
|
1072
|
+
}
|
|
1073
|
+
} else if (event.type === "step-completed") {
|
|
1074
|
+
const item = active.get(key(event.caseId, event.repetition));
|
|
1075
|
+
if (item) item.completedSteps = event.step.index + 1;
|
|
1076
|
+
const passed = event.step.status === "passed";
|
|
1077
|
+
const criterionCount = event.step.criteria.length;
|
|
1078
|
+
const detail = criterionCount ? ` \xB7 ${event.step.criteria.filter((criterion) => criterion.passed).length}/${criterionCount} criteria passed` : "";
|
|
1079
|
+
line(
|
|
1080
|
+
` ${paint(passed ? 32 : 33, passed ? "\u2713" : "\u25C7")} ${text(event.caseId)} #${event.repetition} \xB7 Step ${event.step.index + 1}/${steps.get(event.caseId) ?? "?"} \xB7 ${event.step.observation.type === "interrupt" ? "Human input requested" : text(event.step.observation.type)}${detail}`
|
|
1081
|
+
);
|
|
1082
|
+
} else {
|
|
1083
|
+
const item = event.result;
|
|
1084
|
+
active.delete(key(item.caseId, item.repetition));
|
|
1085
|
+
completed++;
|
|
1086
|
+
const passed = item.status === "passed";
|
|
1087
|
+
const cancelled = item.status === "cancelled";
|
|
1088
|
+
const code = passed ? 32 : cancelled || item.status === "ungraded" ? 33 : 31;
|
|
1089
|
+
const symbol = passed ? "\u2713" : cancelled ? "\u2212" : item.status === "ungraded" ? "\u25C7" : "\u2717";
|
|
1090
|
+
line(
|
|
1091
|
+
` ${paint(code, symbol)} ${text(names.get(item.caseId) ?? item.caseId)} ${paint(90, `[${text(item.caseId)}] #${item.repetition} \xB7 ${duration(item.durationMs)}`)} ${paint(`${code};1`, item.status.toUpperCase())}`
|
|
1092
|
+
);
|
|
1093
|
+
line();
|
|
1094
|
+
}
|
|
1095
|
+
render();
|
|
1096
|
+
},
|
|
1097
|
+
finish(result) {
|
|
1098
|
+
stop();
|
|
1099
|
+
for (const item of result.cases) {
|
|
1100
|
+
for (const step of item.steps) {
|
|
1101
|
+
if (step.status === "passed") continue;
|
|
1102
|
+
line();
|
|
1103
|
+
line(
|
|
1104
|
+
paint(
|
|
1105
|
+
31,
|
|
1106
|
+
` ${text(names.get(item.caseId) ?? item.caseId)} \u203A attempt ${item.repetition} \u203A step ${step.index + 1}`
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
line(
|
|
1110
|
+
` Expected ${step.expectation.type} \xB7 observed ${step.observation.type}`
|
|
1111
|
+
);
|
|
1112
|
+
if (step.reason) line(` ${text(step.reason)}`);
|
|
1113
|
+
for (const criterion of step.criteria.filter(
|
|
1114
|
+
(value) => !value.passed
|
|
1115
|
+
)) {
|
|
1116
|
+
line(` \u2717 ${text(criterion.text)}`);
|
|
1117
|
+
line(` ${text(criterion.reason)}`);
|
|
1118
|
+
for (const evidence of criterion.evidence)
|
|
1119
|
+
line(paint(90, ` Evidence: ${text(evidence)}`));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const issue of item.errors)
|
|
1123
|
+
line(
|
|
1124
|
+
` ${text(item.caseId)} \xB7 ${issue.phase} [${issue.code}] ${text(issue.message)}`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
for (const issue of result.errors)
|
|
1128
|
+
line(` ${issue.phase} [${issue.code}] ${text(issue.message)}`);
|
|
1129
|
+
const successful = result.status === "passed";
|
|
1130
|
+
const code = successful ? 32 : result.status === "cancelled" || result.status === "ungraded" ? 33 : 31;
|
|
1131
|
+
line();
|
|
1132
|
+
if (color) {
|
|
1133
|
+
line(
|
|
1134
|
+
paint(
|
|
1135
|
+
code,
|
|
1136
|
+
` ${progressBar(result.cases.length, result.cases.length, Math.min(22, columns() - 3))} ${result.cases.length}/${result.cases.length}`
|
|
1137
|
+
)
|
|
1138
|
+
);
|
|
1139
|
+
line(
|
|
1140
|
+
paint(
|
|
1141
|
+
`${code};1`,
|
|
1142
|
+
` ${successful ? "\u2713 SUITE PASSED" : `\u25C7 SUITE ${result.status.toUpperCase()}`}`
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
line(
|
|
1147
|
+
` ${paint(result.counts.passed ? 32 : 90, `${result.counts.passed} passed`)} \xB7 ${paint(result.counts.failed ? 31 : 90, `${result.counts.failed} failed`)} \xB7 ${paint(result.counts.error ? 31 : 90, `${result.counts.error} errors`)} \xB7 ${paint(result.counts.cancelled ? 33 : 90, `${result.counts.cancelled} cancelled`)}${result.counts.ungraded ? ` \xB7 ${paint(33, `${result.counts.ungraded} ungraded`)}` : ""}`
|
|
1148
|
+
);
|
|
1149
|
+
line(
|
|
1150
|
+
paint(
|
|
1151
|
+
90,
|
|
1152
|
+
` Duration ${duration(result.durationMs)} \xB7 ${result.cases.length} ${result.cases.length === 1 ? "attempt" : "attempts"}`
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
1155
|
+
line();
|
|
1156
|
+
},
|
|
1157
|
+
pause() {
|
|
1158
|
+
paused = true;
|
|
1159
|
+
clear();
|
|
1160
|
+
},
|
|
1161
|
+
resume() {
|
|
1162
|
+
paused = false;
|
|
1163
|
+
render();
|
|
1164
|
+
},
|
|
1165
|
+
close: stop
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/evals/command.ts
|
|
1170
|
+
var integer = (value) => {
|
|
1171
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
|
|
1172
|
+
throw new import_commander2.InvalidArgumentError("Use a positive integer.");
|
|
1173
|
+
return Number(value);
|
|
1174
|
+
};
|
|
1175
|
+
var isRunner = (value) => value !== null && typeof value === "object" && "run" in value && typeof value.run === "function" && "describe" in value && typeof value.describe === "function";
|
|
1176
|
+
function createLocalEvalsCommand(runtime) {
|
|
1177
|
+
const write = runtime.write ?? process.stdout.write.bind(process.stdout);
|
|
1178
|
+
const exitCode = runtime.setExitCode ?? ((value) => {
|
|
1179
|
+
process.exitCode = value;
|
|
1180
|
+
});
|
|
1181
|
+
const color = runtime.color ?? Boolean(
|
|
1182
|
+
process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0"
|
|
1183
|
+
);
|
|
1184
|
+
const load = async (options) => {
|
|
1185
|
+
const module2 = await runtime.load(
|
|
1186
|
+
(0, import_node_path3.resolve)(runtime.cwd ?? process.cwd(), options.entry),
|
|
1187
|
+
options.env
|
|
1188
|
+
);
|
|
1189
|
+
const selected = options.export ? module2[options.export] : module2.evals ?? module2.default;
|
|
1190
|
+
if (!isRunner(selected))
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
`Export ${options.export ?? '"evals" or default'} must be a createEvals instance.`
|
|
1193
|
+
);
|
|
1194
|
+
const suites = selected.describe().suites.map((suite) => (0, import_agent.parseEvalSuite)(suite));
|
|
1195
|
+
if (!suites.length) throw new Error("No eval suites are configured.");
|
|
1196
|
+
return { runner: selected, suites };
|
|
1197
|
+
};
|
|
1198
|
+
const withRunner = async (options, action) => {
|
|
1199
|
+
try {
|
|
1200
|
+
await action(await load(options));
|
|
1201
|
+
} finally {
|
|
1202
|
+
await runtime.release?.();
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const entryOptions = (command) => command.requiredOption(
|
|
1206
|
+
"--entry <path>",
|
|
1207
|
+
"Module exporting your createEvals instance (TypeScript or JavaScript)."
|
|
1208
|
+
).option("--export <name>", "Named export; defaults to evals or default.").option(
|
|
1209
|
+
"--env <path>",
|
|
1210
|
+
"Load an env file before the entry; repeat for overlays. Replaces automatic env loading.",
|
|
1211
|
+
(value, previous) => [...previous, value],
|
|
1212
|
+
[]
|
|
1213
|
+
).option(
|
|
1214
|
+
"--json",
|
|
1215
|
+
"Print machine-readable JSON instead of the terminal report."
|
|
1216
|
+
);
|
|
1217
|
+
const root = new import_commander2.Command("evals").description(
|
|
1218
|
+
"Run application evals locally, without Studio."
|
|
1219
|
+
);
|
|
1220
|
+
entryOptions(
|
|
1221
|
+
root.command("list").description("List suites exported by an application module.")
|
|
1222
|
+
).action(
|
|
1223
|
+
(options) => withRunner(options, async ({ suites }) => {
|
|
1224
|
+
write(
|
|
1225
|
+
options.json ? `${JSON.stringify(suites)}
|
|
1226
|
+
` : suites.map(
|
|
1227
|
+
(suite) => `${suite.id} ${suite.name ?? suite.id} ${suite.cases.length} cases
|
|
1228
|
+
`
|
|
1229
|
+
).join("")
|
|
1230
|
+
);
|
|
1231
|
+
})
|
|
1232
|
+
);
|
|
1233
|
+
entryOptions(
|
|
1234
|
+
root.command("run").description(
|
|
1235
|
+
"Execute suites and report progress, failures and results locally."
|
|
1236
|
+
)
|
|
1237
|
+
).option(
|
|
1238
|
+
"--suite <id>",
|
|
1239
|
+
"Run one suite; omitted runs all suites in definition order."
|
|
1240
|
+
).option(
|
|
1241
|
+
"--case <id...>",
|
|
1242
|
+
"Select cases within --suite (space-separated IDs)."
|
|
1243
|
+
).option("--repetitions <count>", "Override attempts per case.", integer).option(
|
|
1244
|
+
"--concurrency <count>",
|
|
1245
|
+
"Override concurrent cases within each suite.",
|
|
1246
|
+
integer
|
|
1247
|
+
).option("--no-color", "Disable terminal colors and live progress.").option("--color", "Force terminal colors and live progress.").action(
|
|
1248
|
+
(options, command) => withRunner(options, async ({ runner, suites }) => {
|
|
1249
|
+
const selected = options.suite ? suites.filter((suite) => suite.id === options.suite) : suites;
|
|
1250
|
+
if (!selected.length)
|
|
1251
|
+
throw new Error(`Unknown eval suite: ${options.suite}`);
|
|
1252
|
+
if (options.case && (!options.suite || new Set(options.case).size !== options.case.length || options.case.some(
|
|
1253
|
+
(id) => !selected[0]?.cases.some((item) => item.id === id)
|
|
1254
|
+
)))
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
"--case requires --suite and unique existing case IDs."
|
|
1257
|
+
);
|
|
1258
|
+
const controller = new AbortController();
|
|
1259
|
+
const removeInterrupt = (runtime.onInterrupt ?? ((abort) => {
|
|
1260
|
+
process.once("SIGINT", abort);
|
|
1261
|
+
return () => {
|
|
1262
|
+
process.removeListener("SIGINT", abort);
|
|
1263
|
+
};
|
|
1264
|
+
}))(() => controller.abort());
|
|
1265
|
+
const interactive = options.color && !options.json && (color || command.getOptionValueSource("color") === "cli");
|
|
1266
|
+
const reporter = createEvalTerminalReporter(write, interactive);
|
|
1267
|
+
const restoreOutput = [];
|
|
1268
|
+
if (!runtime.write && interactive) {
|
|
1269
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
1270
|
+
const original = stream.write;
|
|
1271
|
+
const wrapped = new Proxy(original, {
|
|
1272
|
+
apply(target, receiver, args) {
|
|
1273
|
+
reporter.pause();
|
|
1274
|
+
const written = Reflect.apply(target, receiver, args);
|
|
1275
|
+
if (String(args[0]).endsWith("\n")) reporter.resume();
|
|
1276
|
+
return written;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
stream.write = wrapped;
|
|
1280
|
+
restoreOutput.push(() => {
|
|
1281
|
+
if (stream.write === wrapped) stream.write = original;
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const runs = [];
|
|
1286
|
+
try {
|
|
1287
|
+
for (const suite of selected) {
|
|
1288
|
+
if (controller.signal.aborted) break;
|
|
1289
|
+
if (!options.json) reporter.start(suite);
|
|
1290
|
+
const result = await runner.run({
|
|
1291
|
+
suiteId: suite.id,
|
|
1292
|
+
signal: controller.signal,
|
|
1293
|
+
includeActivity: !options.json,
|
|
1294
|
+
...options.case ? { caseIds: options.case } : {},
|
|
1295
|
+
...options.repetitions !== void 0 ? { repetitions: options.repetitions } : {},
|
|
1296
|
+
...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
|
|
1297
|
+
...!options.json ? { onProgress: reporter.progress } : {}
|
|
1298
|
+
});
|
|
1299
|
+
runs.push(result);
|
|
1300
|
+
if (!options.json) reporter.finish(result);
|
|
1301
|
+
}
|
|
1302
|
+
const counts = { passed: 0, failed: 0, error: 0, cancelled: 0 };
|
|
1303
|
+
for (const run2 of runs)
|
|
1304
|
+
for (const key of Object.keys(counts))
|
|
1305
|
+
counts[key] += run2.counts[key];
|
|
1306
|
+
const cancelled = controller.signal.aborted || counts.cancelled > 0;
|
|
1307
|
+
const passed = runs.length === selected.length && runs.every((run2) => run2.status === "passed");
|
|
1308
|
+
const status = cancelled ? "cancelled" : counts.error || runs.some((run2) => run2.status === "error") ? "error" : passed ? "passed" : "failed";
|
|
1309
|
+
if (options.json)
|
|
1310
|
+
write(
|
|
1311
|
+
`${JSON.stringify({ schemaVersion: 1, status, counts, runs })}
|
|
1312
|
+
`
|
|
1313
|
+
);
|
|
1314
|
+
else if (selected.length > 1)
|
|
1315
|
+
write(
|
|
1316
|
+
`Overall ${status.toUpperCase()} \xB7 ${runs.length}/${selected.length} suites \xB7 ${counts.passed} passed \xB7 ${counts.failed} failed \xB7 ${counts.error} errors
|
|
1317
|
+
`
|
|
1318
|
+
);
|
|
1319
|
+
exitCode(cancelled ? 130 : passed ? 0 : 1);
|
|
1320
|
+
} finally {
|
|
1321
|
+
reporter.close();
|
|
1322
|
+
for (const restore of restoreOutput) restore();
|
|
1323
|
+
removeInterrupt();
|
|
1324
|
+
}
|
|
1325
|
+
})
|
|
1326
|
+
);
|
|
1327
|
+
return root;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/evals/environment.ts
|
|
1331
|
+
var import_promises3 = require("fs/promises");
|
|
1332
|
+
var import_node_path4 = require("path");
|
|
1333
|
+
var import_node_util = require("util");
|
|
1334
|
+
var exists = async (file) => {
|
|
1335
|
+
try {
|
|
1336
|
+
await (0, import_promises3.lstat)(file);
|
|
1337
|
+
return true;
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (error.code === "ENOENT") return false;
|
|
1340
|
+
throw new Error(`Cannot read eval configuration: ${file}`);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var findConfiguration = async (cwd) => {
|
|
1344
|
+
let directory = (0, import_node_path4.resolve)(cwd);
|
|
1345
|
+
const directories = [];
|
|
1346
|
+
for (; ; ) {
|
|
1347
|
+
directories.unshift(directory);
|
|
1348
|
+
const profile = (0, import_node_path4.join)(directory, ".env.evals.json");
|
|
1349
|
+
if (await exists(profile)) return { profile, directories };
|
|
1350
|
+
if (await exists((0, import_node_path4.join)(directory, ".git")) || await exists((0, import_node_path4.join)(directory, "pnpm-workspace.yaml")))
|
|
1351
|
+
return { directories };
|
|
1352
|
+
if ((0, import_node_path4.dirname)(directory) === directory)
|
|
1353
|
+
return { directories: [(0, import_node_path4.resolve)(cwd)] };
|
|
1354
|
+
directory = (0, import_node_path4.dirname)(directory);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
var read = async (file, privateFile) => {
|
|
1358
|
+
let stat;
|
|
1359
|
+
try {
|
|
1360
|
+
stat = await (0, import_promises3.lstat)(file);
|
|
1361
|
+
} catch {
|
|
1362
|
+
throw new Error(`Configured eval environment file is unavailable: ${file}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (!stat.isFile() || privateFile && process.platform !== "win32" && ((stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
`Eval configuration must be ${privateFile ? "an owner-only" : "a"} regular file: ${file}`
|
|
1367
|
+
);
|
|
1368
|
+
try {
|
|
1369
|
+
return await (0, import_promises3.readFile)(file, "utf8");
|
|
1370
|
+
} catch {
|
|
1371
|
+
throw new Error(`Cannot read eval configuration: ${file}`);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
async function loadEvalEnvironment(cwd, envFiles, shell = process.env) {
|
|
1375
|
+
let files;
|
|
1376
|
+
let privateFiles = false;
|
|
1377
|
+
if (envFiles?.length) {
|
|
1378
|
+
files = envFiles.map((file) => (0, import_node_path4.resolve)(cwd, file));
|
|
1379
|
+
} else {
|
|
1380
|
+
const { profile, directories } = await findConfiguration(cwd);
|
|
1381
|
+
if (profile) {
|
|
1382
|
+
let value;
|
|
1383
|
+
try {
|
|
1384
|
+
value = JSON.parse(await read(profile, true));
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
if (error instanceof SyntaxError)
|
|
1387
|
+
throw new Error(`Invalid eval environment profile: ${profile}`);
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "envFiles") || !("envFiles" in value) || !Array.isArray(value.envFiles) || !value.envFiles.length || value.envFiles.some(
|
|
1391
|
+
(file) => typeof file !== "string" || !file.trim()
|
|
1392
|
+
))
|
|
1393
|
+
throw new Error(`Invalid eval environment profile: ${profile}`);
|
|
1394
|
+
files = value.envFiles.map(
|
|
1395
|
+
(file) => (0, import_node_path4.resolve)((0, import_node_path4.dirname)(profile), file)
|
|
1396
|
+
);
|
|
1397
|
+
privateFiles = true;
|
|
1398
|
+
} else {
|
|
1399
|
+
files = [];
|
|
1400
|
+
for (const directory of directories)
|
|
1401
|
+
for (const name of [".env", ".env.local"]) {
|
|
1402
|
+
const file = (0, import_node_path4.resolve)(directory, name);
|
|
1403
|
+
if (await exists(file)) files.push(file);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const environment = {};
|
|
1408
|
+
for (const file of files)
|
|
1409
|
+
Object.assign(environment, (0, import_node_util.parseEnv)(await read(file, privateFiles)));
|
|
1410
|
+
return { ...environment, ...shell };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/studio/command.ts
|
|
1414
|
+
var import_commander5 = require("commander");
|
|
1415
|
+
|
|
1416
|
+
// src/studio/eval-command.ts
|
|
1417
|
+
var import_commander3 = require("commander");
|
|
1418
|
+
|
|
1419
|
+
// src/studio/eval-client.ts
|
|
1420
|
+
var import_evals = require("@kortyx/agent/evals");
|
|
1421
|
+
var import_zod3 = require("zod");
|
|
1422
|
+
var StartRequestSchema = import_evals.StudioEvalStartRequestSchema;
|
|
1423
|
+
var parseEvalRunTarget = (input) => {
|
|
1424
|
+
let id = input;
|
|
1425
|
+
let url;
|
|
1426
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
|
|
1427
|
+
let locator;
|
|
1428
|
+
try {
|
|
1429
|
+
locator = new URL(input);
|
|
1430
|
+
} catch {
|
|
1431
|
+
throw new StudioReadError("invalid_target", "Invalid eval run URL.");
|
|
1432
|
+
}
|
|
1433
|
+
const match = locator.pathname.match(/\/evals\/runs\/([^/]+)\/?$/);
|
|
1434
|
+
if (!match || !["http:", "https:"].includes(locator.protocol) || locator.username || locator.password)
|
|
1435
|
+
throw new StudioReadError(
|
|
1436
|
+
"invalid_target",
|
|
1437
|
+
"Expected an HTTP(S) Studio /evals/runs/<uuid> URL without credentials."
|
|
1438
|
+
);
|
|
1439
|
+
id = match[1] ?? "";
|
|
1440
|
+
locator.search = "";
|
|
1441
|
+
locator.hash = "";
|
|
1442
|
+
url = locator.toString();
|
|
1443
|
+
}
|
|
1444
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1445
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1446
|
+
return { id, url };
|
|
1447
|
+
};
|
|
1448
|
+
var StudioEvalClient = class extends StudioApiTransport {
|
|
1449
|
+
targets() {
|
|
1450
|
+
return this.requestJson(
|
|
1451
|
+
"GET",
|
|
1452
|
+
"/v1/studio/evals/targets",
|
|
1453
|
+
import_evals.StudioEvalTargetsResponseSchema
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
runs() {
|
|
1457
|
+
return this.requestJson(
|
|
1458
|
+
"GET",
|
|
1459
|
+
"/v1/studio/evals/runs",
|
|
1460
|
+
import_evals.StudioEvalHistorySchema
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
run(id) {
|
|
1464
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1465
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1466
|
+
return this.requestJson(
|
|
1467
|
+
"GET",
|
|
1468
|
+
`/v1/studio/evals/runs/${id}`,
|
|
1469
|
+
import_evals.StudioEvalDetailSchema
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
start(input) {
|
|
1473
|
+
const parsed = StartRequestSchema.safeParse(input);
|
|
1474
|
+
if (!parsed.success)
|
|
1475
|
+
throw new StudioReadError(
|
|
1476
|
+
"invalid_eval_request",
|
|
1477
|
+
"Invalid suite revision, case selection, or execution limits."
|
|
1478
|
+
);
|
|
1479
|
+
return this.requestJson(
|
|
1480
|
+
"POST",
|
|
1481
|
+
"/v1/studio/evals/runs",
|
|
1482
|
+
import_zod3.z.object({ id: import_zod3.z.uuid() }),
|
|
1483
|
+
{},
|
|
1484
|
+
parsed.data
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
cancel(id) {
|
|
1488
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1489
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1490
|
+
return this.requestJson(
|
|
1491
|
+
"POST",
|
|
1492
|
+
`/v1/studio/evals/runs/${id}/cancel`,
|
|
1493
|
+
import_zod3.z.object({ ok: import_zod3.z.literal(true) })
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
// src/studio/eval-doctor.ts
|
|
1499
|
+
var advice = {
|
|
1500
|
+
environment_forbidden: {
|
|
1501
|
+
message: "Target environment is not allowed in this Studio project.",
|
|
1502
|
+
remedy: "Allow the target environment in the project, or correct the target's environment label."
|
|
1503
|
+
},
|
|
1504
|
+
environment_unavailable: {
|
|
1505
|
+
message: "Studio could not check the target environment.",
|
|
1506
|
+
remedy: "Inspect Studio API database connectivity and private server logs."
|
|
1507
|
+
},
|
|
1508
|
+
endpoint_not_found: {
|
|
1509
|
+
message: "Application endpoint returned HTTP 404.",
|
|
1510
|
+
remedy: "Ensure the eval route is mounted and enabled in the deployed application; check the target URL and reverse proxy route."
|
|
1511
|
+
},
|
|
1512
|
+
endpoint_unauthorized: {
|
|
1513
|
+
message: "Application endpoint rejected discovery credentials.",
|
|
1514
|
+
remedy: "Match the consumer handler service key to the Studio target. If the app authenticates a test actor during discovery, also check that actor's credentials and permissions."
|
|
1515
|
+
},
|
|
1516
|
+
endpoint_http_error: {
|
|
1517
|
+
message: "Application endpoint returned an unsuccessful HTTP status.",
|
|
1518
|
+
remedy: "Inspect the consumer and proxy logs. Check deployment configuration and any app-owned test-identity initialization."
|
|
1519
|
+
},
|
|
1520
|
+
endpoint_unreachable: {
|
|
1521
|
+
message: "Studio API could not fetch the application manifest.",
|
|
1522
|
+
remedy: "Check API-to-consumer networking, DNS, TLS and timeouts. From Docker Desktop use host.docker.internal for a host app; redirects are not followed."
|
|
1523
|
+
},
|
|
1524
|
+
manifest_invalid: {
|
|
1525
|
+
message: "Application returned an empty, oversized or incompatible manifest.",
|
|
1526
|
+
remedy: "Mount createEvalRouteHandler on the exact target URL and use compatible SDK/Studio releases. Check whether a proxy returned HTML instead of JSON."
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
function buildEvalDoctorReport(data, options) {
|
|
1530
|
+
const checks = [
|
|
1531
|
+
{
|
|
1532
|
+
id: "studio_access",
|
|
1533
|
+
status: "passed",
|
|
1534
|
+
message: "Authenticated Studio discovery (studio:read)."
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
id: "execution_permission",
|
|
1538
|
+
status: data.canRun ? "passed" : "failed",
|
|
1539
|
+
message: data.canRun ? "Studio key has eval:run." : "Studio key lacks eval:run.",
|
|
1540
|
+
...!data.canRun ? {
|
|
1541
|
+
remedy: "Grant eval:run to this project key. For local bootstrap rerun with KORTYX_STUDIO_ENABLE_EVALS=1 and the existing stored key."
|
|
1542
|
+
} : {}
|
|
1543
|
+
}
|
|
1544
|
+
];
|
|
1545
|
+
const targets = data.targets.filter(
|
|
1546
|
+
(target) => (!options.target || target.id === options.target) && (!options.environment || target.environment === options.environment)
|
|
1547
|
+
);
|
|
1548
|
+
checks.push({
|
|
1549
|
+
id: "target_selection",
|
|
1550
|
+
status: targets.length ? "passed" : "failed",
|
|
1551
|
+
message: targets.length ? `${targets.length} matching application target(s).` : "No application target matches this connection and selection.",
|
|
1552
|
+
...!targets.length ? {
|
|
1553
|
+
remedy: "Register the target with this key's organization/project and environment. Mount KORTYX_EVAL_TARGETS_FILE on the Studio API, restart it, and check --target/--environment and the connection's default environment."
|
|
1554
|
+
} : {}
|
|
1555
|
+
});
|
|
1556
|
+
for (const target of targets) {
|
|
1557
|
+
const prefix = `${target.id} (${target.environment})`;
|
|
1558
|
+
const diagnostic = target.diagnostic;
|
|
1559
|
+
const environmentFailed = diagnostic?.code === "environment_forbidden" || diagnostic?.code === "environment_unavailable";
|
|
1560
|
+
checks.push({
|
|
1561
|
+
id: `${target.id}:environment`,
|
|
1562
|
+
status: environmentFailed ? "failed" : target.manifest || diagnostic ? "passed" : "skipped",
|
|
1563
|
+
message: `${prefix}: ${environmentFailed ? advice[diagnostic.code].message : target.manifest || diagnostic ? "target environment allowed." : "environment check unavailable on this API."}`,
|
|
1564
|
+
...environmentFailed ? { remedy: advice[diagnostic.code].remedy } : {}
|
|
1565
|
+
});
|
|
1566
|
+
if (!target.manifest) {
|
|
1567
|
+
const guidance = diagnostic && !environmentFailed ? advice[diagnostic.code] : void 0;
|
|
1568
|
+
checks.push({
|
|
1569
|
+
id: `${target.id}:manifest`,
|
|
1570
|
+
status: environmentFailed ? "skipped" : "failed",
|
|
1571
|
+
...diagnostic ? { code: diagnostic.code } : {},
|
|
1572
|
+
message: `${prefix}: ${environmentFailed ? "discovery skipped because the environment check failed." : guidance?.message ?? "consumer unavailable; this API did not supply a failure category."}${diagnostic?.httpStatus ? ` (HTTP ${diagnostic.httpStatus})` : ""}`,
|
|
1573
|
+
...!environmentFailed ? {
|
|
1574
|
+
remedy: guidance?.remedy ?? "Check the deployed consumer route, matching service key and API-to-consumer reachability. Upgrade the Studio API for detailed discovery diagnostics."
|
|
1575
|
+
} : {}
|
|
1576
|
+
});
|
|
1577
|
+
checks.push({
|
|
1578
|
+
id: `${target.id}:judge`,
|
|
1579
|
+
status: "skipped",
|
|
1580
|
+
message: `${prefix}: judge compatibility cannot be checked without a manifest.`
|
|
1581
|
+
});
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
const suites = target.manifest.suites;
|
|
1585
|
+
const selectedSuite = options.suite ? suites.find((suite) => suite.id === options.suite) : void 0;
|
|
1586
|
+
const suitesReady = options.suite ? Boolean(selectedSuite) : suites.length > 0;
|
|
1587
|
+
checks.push({
|
|
1588
|
+
id: `${target.id}:manifest`,
|
|
1589
|
+
status: suitesReady ? "passed" : "failed",
|
|
1590
|
+
message: `${prefix}: authenticated manifest valid; ${suites.length} registered suite(s).${options.suite ? ` Requested suite ${options.suite} ${selectedSuite ? "found" : "missing"}.` : ""}`,
|
|
1591
|
+
...!suitesReady ? {
|
|
1592
|
+
remedy: "Pass the intended suite to createEvals({ suites: [...] }), deploy the application and refresh discovery. Suites are fetched from the consumer; topology publication does not register them."
|
|
1593
|
+
} : {}
|
|
1594
|
+
});
|
|
1595
|
+
const judgeReady = options.judge === "studio" ? Boolean(data.studioJudge && target.manifest.studioJudging) : Boolean(target.manifest.judge);
|
|
1596
|
+
checks.push({
|
|
1597
|
+
id: `${target.id}:judge`,
|
|
1598
|
+
status: judgeReady ? "passed" : "failed",
|
|
1599
|
+
message: `${prefix}: ${options.judge} judge ${judgeReady ? "configured and advertised" : "unavailable"}.`,
|
|
1600
|
+
...!judgeReady ? {
|
|
1601
|
+
remedy: options.judge === "app" ? "Provide a code judge to createEvals in the consumer, or explicitly select --judge studio." : !data.studioJudge ? "Configure KORTYX_EVAL_JUDGE_MODEL and KORTYX_EVAL_JUDGE_API_KEY on the Studio API and restart it, or explicitly select an available --judge app." : "Update the consumer SDK to advertise Studio judging support, or select an available --judge app."
|
|
1602
|
+
} : {}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
return {
|
|
1606
|
+
schemaVersion: 1,
|
|
1607
|
+
status: checks.some((check) => check.status === "failed") ? "failed" : "passed",
|
|
1608
|
+
checks
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function evalDoctorFailure(error) {
|
|
1612
|
+
const safe = error instanceof StudioReadError ? error : null;
|
|
1613
|
+
const connectionError = safe && [
|
|
1614
|
+
"invalid_config",
|
|
1615
|
+
"invalid_connection",
|
|
1616
|
+
"not_configured",
|
|
1617
|
+
"unknown_connection",
|
|
1618
|
+
"invalid_key_env",
|
|
1619
|
+
"missing_key",
|
|
1620
|
+
"invalid_key",
|
|
1621
|
+
"invalid_url",
|
|
1622
|
+
"incompatible_studio_api",
|
|
1623
|
+
"schema_mismatch",
|
|
1624
|
+
"connection_failed"
|
|
1625
|
+
].includes(safe.code);
|
|
1626
|
+
return {
|
|
1627
|
+
schemaVersion: 1,
|
|
1628
|
+
status: "failed",
|
|
1629
|
+
checks: [
|
|
1630
|
+
{
|
|
1631
|
+
id: "studio_access",
|
|
1632
|
+
status: "failed",
|
|
1633
|
+
...safe ? { code: safe.code } : {},
|
|
1634
|
+
message: connectionError ? safe.message : safe?.status === 401 ? "Studio rejected the project key (HTTP 401)." : safe?.status === 403 ? "Studio discovery requires studio:read (HTTP 403)." : safe?.status === 404 ? "Studio eval discovery endpoint was not found (HTTP 404)." : "Could not resolve the Studio connection or read a compatible discovery response.",
|
|
1635
|
+
remedy: "Check the selected connection, its API URL and project key, network access, and compatible CLI/Studio releases. No consumer or provider credentials belong in this CLI connection."
|
|
1636
|
+
}
|
|
1637
|
+
]
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function formatEvalDoctorReport(report) {
|
|
1641
|
+
const symbols = { passed: "\u2713", failed: "\u2717", skipped: "\u2013" };
|
|
1642
|
+
return [
|
|
1643
|
+
"Kortyx Evals \xB7 Setup check",
|
|
1644
|
+
...report.checks.flatMap((check) => [
|
|
1645
|
+
` ${symbols[check.status]} ${check.message}`,
|
|
1646
|
+
...check.remedy ? [` \u2192 ${check.remedy}`] : []
|
|
1647
|
+
]),
|
|
1648
|
+
"",
|
|
1649
|
+
report.status === "passed" ? "Configuration checks passed. Run one representative suite to verify the test identity, tools, model access and saved results." : "Setup checks failed. Resolve the failures and run doctor again.",
|
|
1650
|
+
"No workflow or judge calls were started. Consumer GET discovery may run app-owned authentication logic."
|
|
1651
|
+
].join("\n").replace(/\p{Cc}/gu, (character) => character === "\n" ? character : "");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/studio/read-output.ts
|
|
1655
|
+
var parseStudioTarget = (input, entity) => {
|
|
1656
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
|
|
1657
|
+
let url;
|
|
1658
|
+
try {
|
|
1659
|
+
url = new URL(input);
|
|
1660
|
+
} catch {
|
|
1661
|
+
throw new StudioReadError("invalid_target", "Invalid Studio URL.");
|
|
1662
|
+
}
|
|
1663
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
1664
|
+
throw new StudioReadError(
|
|
1665
|
+
"invalid_target",
|
|
1666
|
+
"Use an HTTP(S) Studio entity URL without embedded credentials."
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const match = url.pathname.match(
|
|
1670
|
+
/\/(runs|sessions|interrupts)\/([^/]+)\/?$/
|
|
1671
|
+
);
|
|
1672
|
+
if (!match)
|
|
1673
|
+
throw new StudioReadError(
|
|
1674
|
+
"invalid_target",
|
|
1675
|
+
"Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
|
|
1676
|
+
);
|
|
1677
|
+
if (entity && match[1] !== entity)
|
|
1678
|
+
throw new StudioReadError(
|
|
1679
|
+
"invalid_target",
|
|
1680
|
+
"URL entity type does not match the command."
|
|
1681
|
+
);
|
|
1682
|
+
let id;
|
|
1683
|
+
try {
|
|
1684
|
+
id = decodeURIComponent(match[2] ?? "");
|
|
1685
|
+
} catch {
|
|
1686
|
+
throw new StudioReadError(
|
|
1687
|
+
"invalid_target",
|
|
1688
|
+
"Invalid entity ID encoding."
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
validateId(id);
|
|
1692
|
+
const selection = {};
|
|
1693
|
+
for (const key of [
|
|
1694
|
+
"tab",
|
|
1695
|
+
"sessionTab",
|
|
1696
|
+
"call",
|
|
1697
|
+
"branch",
|
|
1698
|
+
"node",
|
|
1699
|
+
"event",
|
|
1700
|
+
"trace",
|
|
1701
|
+
"detailView"
|
|
1702
|
+
]) {
|
|
1703
|
+
const value = url.searchParams.get(key);
|
|
1704
|
+
if (value) selection[key] = value;
|
|
1705
|
+
}
|
|
1706
|
+
url.search = "";
|
|
1707
|
+
url.hash = "";
|
|
1708
|
+
return {
|
|
1709
|
+
entity: match[1],
|
|
1710
|
+
id,
|
|
1711
|
+
url: url.toString(),
|
|
1712
|
+
selection
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
if (!entity)
|
|
1716
|
+
throw new StudioReadError(
|
|
1717
|
+
"invalid_target",
|
|
1718
|
+
"inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
|
|
1719
|
+
);
|
|
1720
|
+
validateId(input);
|
|
1721
|
+
return { entity, id: input };
|
|
1722
|
+
};
|
|
1723
|
+
var validateId = (id) => {
|
|
1724
|
+
if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
|
|
1725
|
+
(char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
|
|
1726
|
+
)) {
|
|
1727
|
+
throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
|
|
1731
|
+
var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
|
|
1732
|
+
var sanitizeStudioData = (value, includeContent, key = "") => {
|
|
1733
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
1734
|
+
const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
|
|
1735
|
+
if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
|
|
1736
|
+
if (!includeContent && contentKey.test(normalized))
|
|
1737
|
+
return "[CONTENT OMITTED: use --include-content]";
|
|
1738
|
+
if (typeof value === "string")
|
|
1739
|
+
return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
|
|
1740
|
+
if (Array.isArray(value))
|
|
1741
|
+
return value.map((item) => sanitizeStudioData(item, includeContent));
|
|
1742
|
+
if (value && typeof value === "object")
|
|
1743
|
+
return Object.fromEntries(
|
|
1744
|
+
Object.entries(value).map(([name, item]) => [
|
|
1745
|
+
name,
|
|
1746
|
+
sanitizeStudioData(item, includeContent, name)
|
|
1747
|
+
])
|
|
1748
|
+
);
|
|
1749
|
+
return value;
|
|
1750
|
+
};
|
|
1751
|
+
var summarizeEvidence = (events) => {
|
|
1752
|
+
const findings = events.flatMap((event) => {
|
|
1753
|
+
const payload = event.payload;
|
|
1754
|
+
const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
|
|
1755
|
+
const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
|
|
1756
|
+
String(error.name)
|
|
1757
|
+
));
|
|
1758
|
+
const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
|
|
1759
|
+
const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
|
|
1760
|
+
event.type
|
|
1761
|
+
) || ["denied", "cancelled"].includes(String(payload.outcome));
|
|
1762
|
+
if (!failed && !warning) return [];
|
|
1763
|
+
return [
|
|
1764
|
+
{
|
|
1765
|
+
eventId: event.id,
|
|
1766
|
+
runId: event.runId,
|
|
1767
|
+
nodeId: event.nodeId,
|
|
1768
|
+
type: event.type,
|
|
1769
|
+
occurredAt: event.occurredAt,
|
|
1770
|
+
severity: failed ? "error" : "context",
|
|
1771
|
+
evidence: payload
|
|
1772
|
+
}
|
|
1773
|
+
];
|
|
1774
|
+
});
|
|
1775
|
+
return {
|
|
1776
|
+
findings,
|
|
1777
|
+
note: "Evidence, not an automated root-cause verdict. Interrupts, cancellations, and retries can be expected; correlate with the final run outcome. Missing telemetry does not prove an action did not occur."
|
|
1778
|
+
};
|
|
1779
|
+
};
|
|
1780
|
+
var formatReadOutput = (value, json) => {
|
|
1781
|
+
if (json) return JSON.stringify(value);
|
|
1782
|
+
return JSON.stringify(value, null, 2);
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
// src/studio/eval-command.ts
|
|
1786
|
+
var integer2 = (max) => (value) => {
|
|
1787
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
|
|
1788
|
+
throw new import_commander3.InvalidArgumentError(`Expected an integer between 1 and ${max}.`);
|
|
1789
|
+
return Number(value);
|
|
1790
|
+
};
|
|
1791
|
+
var summary = ({ error: _error, ...run2 }) => run2;
|
|
1792
|
+
var compactDetail = (run2) => ({
|
|
1793
|
+
...summary(run2),
|
|
1794
|
+
suite: void 0,
|
|
1795
|
+
events: void 0,
|
|
1796
|
+
result: void 0,
|
|
1797
|
+
caseResults: run2.result?.cases.map(
|
|
1798
|
+
({ caseId, repetition, status, durationMs, steps }) => ({
|
|
1799
|
+
caseId,
|
|
1800
|
+
repetition,
|
|
1801
|
+
status,
|
|
1802
|
+
durationMs,
|
|
1803
|
+
steps: steps.map(({ index, status: status2, criteria }) => ({
|
|
1804
|
+
index,
|
|
1805
|
+
status: status2,
|
|
1806
|
+
criteria: criteria.map(({ id, passed }) => ({ id, passed }))
|
|
1807
|
+
}))
|
|
1808
|
+
})
|
|
1809
|
+
)
|
|
1810
|
+
});
|
|
1811
|
+
function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
1812
|
+
const common = (command) => command.option(
|
|
1813
|
+
"--connection <name>",
|
|
1814
|
+
"Named project/API connection (or KORTYX_CONNECTION)."
|
|
1815
|
+
).option(
|
|
1816
|
+
"--config-home <path>",
|
|
1817
|
+
"Connection configuration directory.",
|
|
1818
|
+
defaultConnectionsHome()
|
|
1819
|
+
).option(
|
|
1820
|
+
"--home <path>",
|
|
1821
|
+
"Managed local Studio directory.",
|
|
1822
|
+
defaultStudioHome()
|
|
1823
|
+
).option("--api-url <url>", "Direct API URL; requires --api-key-env.").option(
|
|
1824
|
+
"--api-key-env <name>",
|
|
1825
|
+
"Environment variable holding a Studio key."
|
|
1826
|
+
).option("--json", "Print stable machine-readable JSON.").option(
|
|
1827
|
+
"--include-content",
|
|
1828
|
+
"Include suite definitions and captured execution content."
|
|
1829
|
+
);
|
|
1830
|
+
const print = (value, options) => log(
|
|
1831
|
+
formatReadOutput(
|
|
1832
|
+
sanitizeStudioData(
|
|
1833
|
+
{ schemaVersion: 1, ...value },
|
|
1834
|
+
options.includeContent ?? false
|
|
1835
|
+
),
|
|
1836
|
+
options.json ?? false
|
|
1837
|
+
)
|
|
1838
|
+
);
|
|
1839
|
+
const clientFor = async (options, url) => {
|
|
1840
|
+
const connection = await resolveConnection(options, url);
|
|
1841
|
+
return {
|
|
1842
|
+
connection,
|
|
1843
|
+
client: new StudioEvalClient(
|
|
1844
|
+
connection.apiUrl,
|
|
1845
|
+
connection.apiKey,
|
|
1846
|
+
request
|
|
1847
|
+
)
|
|
1848
|
+
};
|
|
1849
|
+
};
|
|
1850
|
+
const evals = studio.command("evals").description(
|
|
1851
|
+
"Discover suites, enqueue evals, inspect saved results and cancel runs."
|
|
1852
|
+
);
|
|
1853
|
+
const suites = evals.command("suites").description("Discover application-owned eval suites.");
|
|
1854
|
+
const discover = async (options) => {
|
|
1855
|
+
const { connection, client } = await clientFor(options);
|
|
1856
|
+
const data = await client.targets();
|
|
1857
|
+
const environment = options.environment ?? connection.environment;
|
|
1858
|
+
const targets = data.targets.filter(
|
|
1859
|
+
(target) => (!options.target || target.id === options.target) && (!environment || target.environment === environment)
|
|
1860
|
+
);
|
|
1861
|
+
return {
|
|
1862
|
+
connection,
|
|
1863
|
+
client,
|
|
1864
|
+
targets,
|
|
1865
|
+
canRun: data.canRun,
|
|
1866
|
+
studioJudge: data.studioJudge ?? null
|
|
1867
|
+
};
|
|
1868
|
+
};
|
|
1869
|
+
const selectionOptions = (command) => common(command).option("--target <id>", "Application eval target ID.").option(
|
|
1870
|
+
"--environment <name>",
|
|
1871
|
+
"Filter targets; defaults to the connection environment."
|
|
1872
|
+
);
|
|
1873
|
+
selectionOptions(
|
|
1874
|
+
evals.command("doctor").description(
|
|
1875
|
+
"Check deployment wiring without starting workflows or model calls."
|
|
1876
|
+
)
|
|
1877
|
+
).option("--suite <id>", "Check that a specific suite is registered.").option(
|
|
1878
|
+
"--judge <location>",
|
|
1879
|
+
"Judge to check: studio (default) or app.",
|
|
1880
|
+
(value) => {
|
|
1881
|
+
if (value !== "studio" && value !== "app")
|
|
1882
|
+
throw new import_commander3.InvalidArgumentError("Expected studio or app.");
|
|
1883
|
+
return value;
|
|
1884
|
+
},
|
|
1885
|
+
"studio"
|
|
1886
|
+
).action(async (options) => {
|
|
1887
|
+
let report;
|
|
1888
|
+
try {
|
|
1889
|
+
const { connection, client } = await clientFor(options);
|
|
1890
|
+
report = buildEvalDoctorReport(await client.targets(), {
|
|
1891
|
+
...options,
|
|
1892
|
+
environment: options.environment ?? connection.environment,
|
|
1893
|
+
judge: options.judge ?? "studio"
|
|
1894
|
+
});
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
report = evalDoctorFailure(error);
|
|
1897
|
+
}
|
|
1898
|
+
log(
|
|
1899
|
+
options.json ? JSON.stringify(report) : formatEvalDoctorReport(report)
|
|
1900
|
+
);
|
|
1901
|
+
if (report.status === "failed") process.exitCode = 1;
|
|
1902
|
+
});
|
|
1903
|
+
selectionOptions(
|
|
1904
|
+
suites.command("list").description("List targets, suites, revisions and case IDs.")
|
|
1905
|
+
).action(async (options) => {
|
|
1906
|
+
const { connection, targets, canRun, studioJudge } = await discover(options);
|
|
1907
|
+
print(
|
|
1908
|
+
{
|
|
1909
|
+
connection: connection.name,
|
|
1910
|
+
canRun,
|
|
1911
|
+
studioJudge,
|
|
1912
|
+
targets: targets.map((target) => ({
|
|
1913
|
+
id: target.id,
|
|
1914
|
+
name: target.name,
|
|
1915
|
+
environment: target.environment,
|
|
1916
|
+
available: target.manifest !== null,
|
|
1917
|
+
appJudge: target.manifest?.judge ?? null,
|
|
1918
|
+
studioJudging: Boolean(target.manifest?.studioJudging),
|
|
1919
|
+
suites: target.manifest?.suites.map((suite) => ({
|
|
1920
|
+
id: suite.id,
|
|
1921
|
+
name: suite.name,
|
|
1922
|
+
revision: target.revisions[suite.id],
|
|
1923
|
+
caseIds: suite.cases.map((item) => item.id)
|
|
1924
|
+
})) ?? []
|
|
1925
|
+
}))
|
|
1926
|
+
},
|
|
1927
|
+
options
|
|
1928
|
+
);
|
|
1929
|
+
});
|
|
1930
|
+
const select = async (suiteId, options) => {
|
|
1931
|
+
const data = await discover(options);
|
|
1932
|
+
const matches = data.targets.flatMap((target) => {
|
|
1933
|
+
const suite = target.manifest?.suites.find((item) => item.id === suiteId);
|
|
1934
|
+
return suite ? [{ target, suite }] : [];
|
|
1935
|
+
});
|
|
1936
|
+
const match = matches[0];
|
|
1937
|
+
if (matches.length !== 1 || !match)
|
|
848
1938
|
throw new StudioReadError(
|
|
849
|
-
"
|
|
850
|
-
"
|
|
1939
|
+
"invalid_suite_selection",
|
|
1940
|
+
matches.length ? "Suite exists on multiple targets. Select --target and/or --environment." : "Suite unavailable. List suites and check the target connection."
|
|
851
1941
|
);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1942
|
+
return { ...data, ...match };
|
|
1943
|
+
};
|
|
1944
|
+
selectionOptions(
|
|
1945
|
+
suites.command("get <suite-id>").description(
|
|
1946
|
+
"Read a suite; --include-content returns its full definition."
|
|
1947
|
+
)
|
|
1948
|
+
).action(async (suiteId, options) => {
|
|
1949
|
+
const { connection, target, suite } = await select(suiteId, options);
|
|
1950
|
+
print(
|
|
1951
|
+
{
|
|
1952
|
+
connection: connection.name,
|
|
1953
|
+
targetId: target.id,
|
|
1954
|
+
revision: target.revisions[suite.id],
|
|
1955
|
+
suite: options.includeContent ? suite : {
|
|
1956
|
+
id: suite.id,
|
|
1957
|
+
name: suite.name,
|
|
1958
|
+
caseIds: suite.cases.map((item) => item.id)
|
|
1959
|
+
}
|
|
1960
|
+
},
|
|
1961
|
+
options
|
|
1962
|
+
);
|
|
856
1963
|
});
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1964
|
+
const runs = evals.command("runs").description("Start and inspect saved eval runs.");
|
|
1965
|
+
selectionOptions(
|
|
1966
|
+
runs.command("start <suite-id>").description(
|
|
1967
|
+
"Enqueue once and return immediately; does not wait for grades."
|
|
860
1968
|
)
|
|
861
|
-
).
|
|
862
|
-
|
|
863
|
-
|
|
1969
|
+
).option(
|
|
1970
|
+
"--case <id>",
|
|
1971
|
+
"Select a case; repeat for multiple cases.",
|
|
1972
|
+
(value, previous) => [...previous, value],
|
|
1973
|
+
[]
|
|
1974
|
+
).option(
|
|
1975
|
+
"--repetitions <count>",
|
|
1976
|
+
"Attempts per selected case (1\u201320).",
|
|
1977
|
+
integer2(20),
|
|
1978
|
+
1
|
|
1979
|
+
).option("--concurrency <count>", "Parallel attempts (1\u20134).", integer2(4), 1).option(
|
|
1980
|
+
"--judge <location>",
|
|
1981
|
+
"Judge selection: studio (default) or app.",
|
|
1982
|
+
(value) => {
|
|
1983
|
+
if (value !== "studio" && value !== "app")
|
|
1984
|
+
throw new import_commander3.InvalidArgumentError("Expected studio or app.");
|
|
1985
|
+
return value;
|
|
1986
|
+
},
|
|
1987
|
+
"studio"
|
|
1988
|
+
).action(async (suiteId, options) => {
|
|
1989
|
+
const { connection, client, target, suite, canRun } = await select(
|
|
1990
|
+
suiteId,
|
|
1991
|
+
options
|
|
1992
|
+
);
|
|
1993
|
+
if (!canRun)
|
|
864
1994
|
throw new StudioReadError(
|
|
865
|
-
"
|
|
866
|
-
"
|
|
1995
|
+
"eval_forbidden",
|
|
1996
|
+
"This Studio key requires eval:run to execute suites."
|
|
867
1997
|
);
|
|
868
|
-
|
|
869
|
-
|
|
1998
|
+
const caseIds = options.case ?? [];
|
|
1999
|
+
if (new Set(caseIds).size !== caseIds.length || caseIds.some((id) => !suite.cases.some((item) => item.id === id)) || (caseIds.length || suite.cases.length) * options.repetitions > 100)
|
|
2000
|
+
throw new StudioReadError(
|
|
2001
|
+
"invalid_case_selection",
|
|
2002
|
+
"Select unique existing case IDs and at most 100 total attempts."
|
|
2003
|
+
);
|
|
2004
|
+
const run2 = await client.start({
|
|
2005
|
+
targetId: target.id,
|
|
2006
|
+
judge: options.judge ?? "studio",
|
|
2007
|
+
suiteId,
|
|
2008
|
+
suiteRevision: target.revisions[suiteId] ?? "",
|
|
2009
|
+
repetitions: options.repetitions,
|
|
2010
|
+
concurrency: options.concurrency,
|
|
2011
|
+
...caseIds.length ? { caseIds } : {}
|
|
2012
|
+
});
|
|
2013
|
+
print(
|
|
2014
|
+
{
|
|
2015
|
+
connection: connection.name,
|
|
2016
|
+
id: run2.id,
|
|
2017
|
+
status: "queued",
|
|
2018
|
+
studioUrl: connection.studioUrl ? `${connection.studioUrl}/evals/runs/${run2.id}` : null
|
|
2019
|
+
},
|
|
2020
|
+
options
|
|
870
2021
|
);
|
|
871
|
-
if (config.current === name) delete config.current;
|
|
872
|
-
await saveConnections(config, options.configHome);
|
|
873
|
-
print({ removed: name }, options);
|
|
874
2022
|
});
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
2023
|
+
common(
|
|
2024
|
+
runs.command("list").description("List the latest 100 saved runs in this project.")
|
|
2025
|
+
).option(
|
|
2026
|
+
"--environment <name>",
|
|
2027
|
+
"Filter results; defaults to the connection environment."
|
|
2028
|
+
).action(async (options) => {
|
|
2029
|
+
const { connection, client } = await clientFor(options);
|
|
2030
|
+
const data = await client.runs();
|
|
2031
|
+
const environment = options.environment ?? connection.environment;
|
|
2032
|
+
print(
|
|
2033
|
+
{
|
|
2034
|
+
connection: connection.name,
|
|
2035
|
+
runs: data.runs.filter((run2) => !environment || run2.environment === environment).map(summary)
|
|
2036
|
+
},
|
|
2037
|
+
options
|
|
2038
|
+
);
|
|
2039
|
+
});
|
|
2040
|
+
common(
|
|
2041
|
+
runs.command("get <id-or-url>").description(
|
|
2042
|
+
"Read status, counts and criterion verdicts; content is opt-in."
|
|
2043
|
+
)
|
|
2044
|
+
).action(async (input, options) => {
|
|
2045
|
+
const target = parseEvalRunTarget(input);
|
|
2046
|
+
const { connection, client } = await clientFor(options, target.url);
|
|
2047
|
+
const { run: run2 } = await client.run(target.id);
|
|
2048
|
+
print(
|
|
2049
|
+
{
|
|
2050
|
+
connection: connection.name,
|
|
2051
|
+
run: options.includeContent ? run2 : compactDetail(run2)
|
|
2052
|
+
},
|
|
2053
|
+
options
|
|
2054
|
+
);
|
|
2055
|
+
});
|
|
2056
|
+
common(
|
|
2057
|
+
runs.command("cancel <id-or-url>").description(
|
|
2058
|
+
"Request cooperative cancellation of a queued or running eval."
|
|
2059
|
+
)
|
|
2060
|
+
).action(async (input, options) => {
|
|
2061
|
+
const target = parseEvalRunTarget(input);
|
|
2062
|
+
const { connection, client } = await clientFor(options, target.url);
|
|
2063
|
+
const data = await client.cancel(target.id);
|
|
2064
|
+
print({ connection: connection.name, id: target.id, ...data }, options);
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
880
2067
|
|
|
881
2068
|
// src/studio/local-stack.ts
|
|
882
|
-
var
|
|
2069
|
+
var import_promises4 = require("fs/promises");
|
|
883
2070
|
var import_telemetry_contracts3 = require("@kortyx/telemetry-contracts");
|
|
884
2071
|
var composeArgs = (home, args) => [
|
|
885
2072
|
"compose",
|
|
@@ -1033,9 +2220,20 @@ var startStudio = async (options, runtime) => {
|
|
|
1033
2220
|
const studioImage = String(
|
|
1034
2221
|
environment.KORTYX_STUDIO_IMAGE_REF ?? `${environment.KORTYX_STUDIO_IMAGE ?? "ghcr.io/kortyx-io/kortyx-studio"}:${config.imageTag}`
|
|
1035
2222
|
);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
2223
|
+
const pullPolicy = process.env.KORTYX_STUDIO_PULL_POLICY ?? "always";
|
|
2224
|
+
for (const image of /* @__PURE__ */ new Set([apiImage, studioImage])) {
|
|
2225
|
+
if (pullPolicy === "never") continue;
|
|
2226
|
+
if (pullPolicy === "missing") {
|
|
2227
|
+
try {
|
|
2228
|
+
await runtime.run("docker", ["image", "inspect", image], {
|
|
2229
|
+
inherit: false
|
|
2230
|
+
});
|
|
2231
|
+
continue;
|
|
2232
|
+
} catch {
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
await runtime.run("docker", ["pull", image], { inherit: true });
|
|
2236
|
+
}
|
|
1039
2237
|
const capability = await runtime.run(
|
|
1040
2238
|
"docker",
|
|
1041
2239
|
[
|
|
@@ -1049,7 +2247,7 @@ var startStudio = async (options, runtime) => {
|
|
|
1049
2247
|
],
|
|
1050
2248
|
{ inherit: false }
|
|
1051
2249
|
);
|
|
1052
|
-
await (0,
|
|
2250
|
+
await (0, import_promises4.writeFile)(
|
|
1053
2251
|
studioComposePath(options.home),
|
|
1054
2252
|
studioComposeFile(
|
|
1055
2253
|
process.platform !== "win32" && capability.stdout.trim() === "supported"
|
|
@@ -1152,7 +2350,7 @@ var resetStudio = async (home, confirm, runtime) => {
|
|
|
1152
2350
|
|
|
1153
2351
|
// src/studio/read-command.ts
|
|
1154
2352
|
var import_telemetry_contracts5 = require("@kortyx/telemetry-contracts");
|
|
1155
|
-
var
|
|
2353
|
+
var import_commander4 = require("commander");
|
|
1156
2354
|
|
|
1157
2355
|
// src/studio/read-analysis.ts
|
|
1158
2356
|
var import_telemetry_contracts4 = require("@kortyx/telemetry-contracts");
|
|
@@ -1459,141 +2657,10 @@ var compareRunAnalysis = (left, right) => {
|
|
|
1459
2657
|
};
|
|
1460
2658
|
};
|
|
1461
2659
|
|
|
1462
|
-
// src/studio/read-output.ts
|
|
1463
|
-
var parseStudioTarget = (input, entity) => {
|
|
1464
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
|
|
1465
|
-
let url;
|
|
1466
|
-
try {
|
|
1467
|
-
url = new URL(input);
|
|
1468
|
-
} catch {
|
|
1469
|
-
throw new StudioReadError("invalid_target", "Invalid Studio URL.");
|
|
1470
|
-
}
|
|
1471
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
1472
|
-
throw new StudioReadError(
|
|
1473
|
-
"invalid_target",
|
|
1474
|
-
"Use an HTTP(S) Studio entity URL without embedded credentials."
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
const match = url.pathname.match(
|
|
1478
|
-
/\/(runs|sessions|interrupts)\/([^/]+)\/?$/
|
|
1479
|
-
);
|
|
1480
|
-
if (!match)
|
|
1481
|
-
throw new StudioReadError(
|
|
1482
|
-
"invalid_target",
|
|
1483
|
-
"Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
|
|
1484
|
-
);
|
|
1485
|
-
if (entity && match[1] !== entity)
|
|
1486
|
-
throw new StudioReadError(
|
|
1487
|
-
"invalid_target",
|
|
1488
|
-
"URL entity type does not match the command."
|
|
1489
|
-
);
|
|
1490
|
-
let id;
|
|
1491
|
-
try {
|
|
1492
|
-
id = decodeURIComponent(match[2] ?? "");
|
|
1493
|
-
} catch {
|
|
1494
|
-
throw new StudioReadError(
|
|
1495
|
-
"invalid_target",
|
|
1496
|
-
"Invalid entity ID encoding."
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
validateId(id);
|
|
1500
|
-
const selection = {};
|
|
1501
|
-
for (const key of [
|
|
1502
|
-
"tab",
|
|
1503
|
-
"sessionTab",
|
|
1504
|
-
"call",
|
|
1505
|
-
"branch",
|
|
1506
|
-
"node",
|
|
1507
|
-
"event",
|
|
1508
|
-
"trace",
|
|
1509
|
-
"detailView"
|
|
1510
|
-
]) {
|
|
1511
|
-
const value = url.searchParams.get(key);
|
|
1512
|
-
if (value) selection[key] = value;
|
|
1513
|
-
}
|
|
1514
|
-
url.search = "";
|
|
1515
|
-
url.hash = "";
|
|
1516
|
-
return {
|
|
1517
|
-
entity: match[1],
|
|
1518
|
-
id,
|
|
1519
|
-
url: url.toString(),
|
|
1520
|
-
selection
|
|
1521
|
-
};
|
|
1522
|
-
}
|
|
1523
|
-
if (!entity)
|
|
1524
|
-
throw new StudioReadError(
|
|
1525
|
-
"invalid_target",
|
|
1526
|
-
"inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
|
|
1527
|
-
);
|
|
1528
|
-
validateId(input);
|
|
1529
|
-
return { entity, id: input };
|
|
1530
|
-
};
|
|
1531
|
-
var validateId = (id) => {
|
|
1532
|
-
if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
|
|
1533
|
-
(char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
|
|
1534
|
-
)) {
|
|
1535
|
-
throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
|
|
1539
|
-
var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
|
|
1540
|
-
var sanitizeStudioData = (value, includeContent, key = "") => {
|
|
1541
|
-
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
1542
|
-
const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
|
|
1543
|
-
if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
|
|
1544
|
-
if (!includeContent && contentKey.test(normalized))
|
|
1545
|
-
return "[CONTENT OMITTED: use --include-content]";
|
|
1546
|
-
if (typeof value === "string")
|
|
1547
|
-
return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
|
|
1548
|
-
if (Array.isArray(value))
|
|
1549
|
-
return value.map((item) => sanitizeStudioData(item, includeContent));
|
|
1550
|
-
if (value && typeof value === "object")
|
|
1551
|
-
return Object.fromEntries(
|
|
1552
|
-
Object.entries(value).map(([name, item]) => [
|
|
1553
|
-
name,
|
|
1554
|
-
sanitizeStudioData(item, includeContent, name)
|
|
1555
|
-
])
|
|
1556
|
-
);
|
|
1557
|
-
return value;
|
|
1558
|
-
};
|
|
1559
|
-
var summarizeEvidence = (events) => {
|
|
1560
|
-
const findings = events.flatMap((event) => {
|
|
1561
|
-
const payload = event.payload;
|
|
1562
|
-
const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
|
|
1563
|
-
const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
|
|
1564
|
-
String(error.name)
|
|
1565
|
-
));
|
|
1566
|
-
const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
|
|
1567
|
-
const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
|
|
1568
|
-
event.type
|
|
1569
|
-
) || ["denied", "cancelled"].includes(String(payload.outcome));
|
|
1570
|
-
if (!failed && !warning) return [];
|
|
1571
|
-
return [
|
|
1572
|
-
{
|
|
1573
|
-
eventId: event.id,
|
|
1574
|
-
runId: event.runId,
|
|
1575
|
-
nodeId: event.nodeId,
|
|
1576
|
-
type: event.type,
|
|
1577
|
-
occurredAt: event.occurredAt,
|
|
1578
|
-
severity: failed ? "error" : "context",
|
|
1579
|
-
evidence: payload
|
|
1580
|
-
}
|
|
1581
|
-
];
|
|
1582
|
-
});
|
|
1583
|
-
return {
|
|
1584
|
-
findings,
|
|
1585
|
-
note: "Evidence, not an automated root-cause verdict. Interrupts, cancellations, and retries can be expected; correlate with the final run outcome. Missing telemetry does not prove an action did not occur."
|
|
1586
|
-
};
|
|
1587
|
-
};
|
|
1588
|
-
var formatReadOutput = (value, json) => {
|
|
1589
|
-
if (json) return JSON.stringify(value);
|
|
1590
|
-
return JSON.stringify(value, null, 2);
|
|
1591
|
-
};
|
|
1592
|
-
|
|
1593
2660
|
// src/studio/read-command.ts
|
|
1594
|
-
var
|
|
2661
|
+
var integer3 = (min, max) => (value) => {
|
|
1595
2662
|
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
|
|
1596
|
-
throw new
|
|
2663
|
+
throw new import_commander4.InvalidArgumentError(
|
|
1597
2664
|
`Expected an integer between ${min} and ${max}.`
|
|
1598
2665
|
);
|
|
1599
2666
|
}
|
|
@@ -1749,7 +2816,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1749
2816
|
const detailOptions = (command) => common(command).option(
|
|
1750
2817
|
"--event-limit <count>",
|
|
1751
2818
|
"Maximum latest events and diagnostic findings (1\u201310000).",
|
|
1752
|
-
|
|
2819
|
+
integer3(1, 1e4),
|
|
1753
2820
|
100
|
|
1754
2821
|
).option(
|
|
1755
2822
|
"--focus-selection",
|
|
@@ -1802,7 +2869,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1802
2869
|
"--environment <name>",
|
|
1803
2870
|
"Environment filter; overrides the connection default."
|
|
1804
2871
|
).addOption(
|
|
1805
|
-
new
|
|
2872
|
+
new import_commander4.Option(
|
|
1806
2873
|
"--range <range>",
|
|
1807
2874
|
"Studio time range: 1h, 24h, 7d, 30d, all (or Studio preset name)."
|
|
1808
2875
|
).choices([
|
|
@@ -1847,7 +2914,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1847
2914
|
).option(
|
|
1848
2915
|
"--event-limit <count>",
|
|
1849
2916
|
"Maximum timeline steps returned per run (1\u201310000).",
|
|
1850
|
-
|
|
2917
|
+
integer3(1, 1e4),
|
|
1851
2918
|
100
|
|
1852
2919
|
).action(
|
|
1853
2920
|
async (leftInput, rightInput, options) => {
|
|
@@ -1947,13 +3014,13 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1947
3014
|
}
|
|
1948
3015
|
const list = filters(
|
|
1949
3016
|
group.command("list").description("Read a bounded, filtered page.")
|
|
1950
|
-
).option("--limit <count>", "Page size (1\u2013100).",
|
|
3017
|
+
).option("--limit <count>", "Page size (1\u2013100).", integer3(1, 100), 25).option(
|
|
1951
3018
|
"--cursor <offset>",
|
|
1952
3019
|
"Numeric offset returned as nextCursor.",
|
|
1953
|
-
|
|
3020
|
+
integer3(0, Number.MAX_SAFE_INTEGER),
|
|
1954
3021
|
0
|
|
1955
3022
|
).option("--status <status>", "Entity status filter.").option("--session <id>", "Session filter.").option("--user <id>", "User filter.").option("--tenant <id>", "Tenant filter.").option("--model <name>", "Model filter.").option("--node <id>", "Node filter.").option("--tool <name>", "Tool name filter.").addOption(
|
|
1956
|
-
new
|
|
3023
|
+
new import_commander4.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
|
|
1957
3024
|
"success",
|
|
1958
3025
|
"denied",
|
|
1959
3026
|
"fault",
|
|
@@ -2082,14 +3149,14 @@ var defaultStudioRuntime = {
|
|
|
2082
3149
|
var portParser = (value) => {
|
|
2083
3150
|
const port = Number(value);
|
|
2084
3151
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2085
|
-
throw new
|
|
3152
|
+
throw new import_commander5.InvalidArgumentError(
|
|
2086
3153
|
"Port must be an integer between 1 and 65535."
|
|
2087
3154
|
);
|
|
2088
3155
|
}
|
|
2089
3156
|
return port;
|
|
2090
3157
|
};
|
|
2091
3158
|
var withHome = (command) => command.addOption(
|
|
2092
|
-
new
|
|
3159
|
+
new import_commander5.Option("--home <path>", "Local Studio state directory.").default(
|
|
2093
3160
|
defaultStudioHome()
|
|
2094
3161
|
)
|
|
2095
3162
|
);
|
|
@@ -2099,7 +3166,7 @@ var configureOutput = (command, runtime) => command.configureOutput({
|
|
|
2099
3166
|
});
|
|
2100
3167
|
var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
2101
3168
|
const studio = configureOutput(
|
|
2102
|
-
new
|
|
3169
|
+
new import_commander5.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
|
|
2103
3170
|
"after",
|
|
2104
3171
|
"\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
|
|
2105
3172
|
),
|
|
@@ -2142,7 +3209,7 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
|
2142
3209
|
"Generate unpersisted credentials for a self-hosted deployment.",
|
|
2143
3210
|
false
|
|
2144
3211
|
).addOption(
|
|
2145
|
-
new
|
|
3212
|
+
new import_commander5.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
|
|
2146
3213
|
).option(
|
|
2147
3214
|
"--service-name <name>",
|
|
2148
3215
|
"SDK service name used by dotenv output.",
|
|
@@ -2184,20 +3251,29 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
|
2184
3251
|
).action(async ({ home, confirm }) => {
|
|
2185
3252
|
await resetStudio(home, confirm, runtime);
|
|
2186
3253
|
});
|
|
2187
|
-
registerStudioReadCommands(
|
|
3254
|
+
registerStudioReadCommands(
|
|
3255
|
+
studio,
|
|
3256
|
+
(message) => runtime.log(message),
|
|
3257
|
+
runtime.request
|
|
3258
|
+
);
|
|
3259
|
+
registerStudioEvalCommands(
|
|
3260
|
+
studio,
|
|
3261
|
+
(message) => runtime.log(message),
|
|
3262
|
+
runtime.request
|
|
3263
|
+
);
|
|
2188
3264
|
return studio;
|
|
2189
3265
|
};
|
|
2190
3266
|
|
|
2191
3267
|
// src/workflow-calls.ts
|
|
2192
|
-
var
|
|
3268
|
+
var import_node_path5 = require("path");
|
|
2193
3269
|
var import_typescript = __toESM(require("typescript"));
|
|
2194
3270
|
function discoverWorkflowCalls(entry, snapshots) {
|
|
2195
|
-
const configPath = import_typescript.default.findConfigFile((0,
|
|
3271
|
+
const configPath = import_typescript.default.findConfigFile((0, import_node_path5.dirname)(entry), import_typescript.default.sys.fileExists);
|
|
2196
3272
|
const config = configPath ? import_typescript.default.readConfigFile(configPath, import_typescript.default.sys.readFile).config : {};
|
|
2197
3273
|
const parsed = import_typescript.default.parseJsonConfigFileContent(
|
|
2198
3274
|
config ?? {},
|
|
2199
3275
|
import_typescript.default.sys,
|
|
2200
|
-
configPath ? (0,
|
|
3276
|
+
configPath ? (0, import_node_path5.dirname)(configPath) : (0, import_node_path5.dirname)(entry)
|
|
2201
3277
|
);
|
|
2202
3278
|
const program = import_typescript.default.createProgram([entry], {
|
|
2203
3279
|
...parsed.options,
|
|
@@ -2238,7 +3314,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2238
3314
|
);
|
|
2239
3315
|
};
|
|
2240
3316
|
const factoryBindings = /* @__PURE__ */ new WeakMap();
|
|
2241
|
-
function
|
|
3317
|
+
function resolve6(e, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
2242
3318
|
if (!e) return;
|
|
2243
3319
|
e = unwrap(e);
|
|
2244
3320
|
if (seen.has(e)) return;
|
|
@@ -2246,18 +3322,18 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2246
3322
|
if (import_typescript.default.isIdentifier(e)) {
|
|
2247
3323
|
const s = symbol(e);
|
|
2248
3324
|
const bound = s && bindings.get(s);
|
|
2249
|
-
if (bound) return
|
|
3325
|
+
if (bound) return resolve6(bound, bindings, seen);
|
|
2250
3326
|
const d = s?.valueDeclaration;
|
|
2251
3327
|
if (d && (import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isPropertyAssignment(d)) && d.initializer && (!import_typescript.default.isVariableDeclaration(d) || import_typescript.default.isVariableDeclarationList(d.parent) && (d.parent.flags & import_typescript.default.NodeFlags.Const) !== 0))
|
|
2252
|
-
return
|
|
3328
|
+
return resolve6(d.initializer, bindings, seen);
|
|
2253
3329
|
}
|
|
2254
3330
|
if (import_typescript.default.isPropertyAccessExpression(e)) {
|
|
2255
3331
|
const found = property(e.expression, e.name.text, bindings, seen);
|
|
2256
|
-
if (found) return
|
|
3332
|
+
if (found) return resolve6(found, bindings, seen);
|
|
2257
3333
|
const owner = symbol(e.expression);
|
|
2258
3334
|
const declaration = symbol(e.name)?.valueDeclaration;
|
|
2259
3335
|
if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
|
|
2260
|
-
return
|
|
3336
|
+
return resolve6(declaration.initializer, bindings, seen);
|
|
2261
3337
|
return e;
|
|
2262
3338
|
}
|
|
2263
3339
|
if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
|
|
@@ -2267,7 +3343,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2267
3343
|
bindings,
|
|
2268
3344
|
seen
|
|
2269
3345
|
);
|
|
2270
|
-
return found ?
|
|
3346
|
+
return found ? resolve6(found, bindings, seen) : e;
|
|
2271
3347
|
}
|
|
2272
3348
|
if (import_typescript.default.isCallExpression(e)) {
|
|
2273
3349
|
const d = symbol(
|
|
@@ -2281,12 +3357,12 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2281
3357
|
const key = symbol(p.name);
|
|
2282
3358
|
const arg = e.arguments[i] ?? p.initializer;
|
|
2283
3359
|
if (key && arg)
|
|
2284
|
-
bound.set(key,
|
|
3360
|
+
bound.set(key, resolve6(arg, bindings, new Set(seen)) ?? arg);
|
|
2285
3361
|
}
|
|
2286
3362
|
});
|
|
2287
3363
|
const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
|
|
2288
3364
|
if (returned.length === 1 && returned[0]) {
|
|
2289
|
-
const value =
|
|
3365
|
+
const value = resolve6(returned[0], bound, seen);
|
|
2290
3366
|
if (value) factoryBindings.set(value, bound);
|
|
2291
3367
|
return value;
|
|
2292
3368
|
}
|
|
@@ -2295,30 +3371,30 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2295
3371
|
return e;
|
|
2296
3372
|
}
|
|
2297
3373
|
function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
2298
|
-
const obj =
|
|
3374
|
+
const obj = resolve6(e, bindings, seen);
|
|
2299
3375
|
if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
|
|
2300
3376
|
const scoped = factoryBindings.get(obj) ?? bindings;
|
|
2301
3377
|
for (const p of [...obj.properties].reverse()) {
|
|
2302
3378
|
if (import_typescript.default.isSpreadAssignment(p)) {
|
|
2303
|
-
const spread =
|
|
3379
|
+
const spread = resolve6(p.expression, scoped, new Set(seen));
|
|
2304
3380
|
if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
|
|
2305
3381
|
const found = property(p.expression, key, scoped, new Set(seen));
|
|
2306
3382
|
if (found) return found;
|
|
2307
3383
|
} else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
|
|
2308
3384
|
if (import_typescript.default.isPropertyAssignment(p))
|
|
2309
|
-
return
|
|
3385
|
+
return resolve6(p.initializer, scoped, new Set(seen));
|
|
2310
3386
|
if (import_typescript.default.isShorthandPropertyAssignment(p))
|
|
2311
|
-
return
|
|
3387
|
+
return resolve6(p.name, scoped, new Set(seen));
|
|
2312
3388
|
}
|
|
2313
3389
|
}
|
|
2314
3390
|
return void 0;
|
|
2315
3391
|
}
|
|
2316
3392
|
function literal(e, bindings) {
|
|
2317
|
-
const value =
|
|
3393
|
+
const value = resolve6(e, bindings);
|
|
2318
3394
|
return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
|
|
2319
3395
|
}
|
|
2320
3396
|
function workflowId(e, bindings) {
|
|
2321
|
-
const value =
|
|
3397
|
+
const value = resolve6(e, bindings);
|
|
2322
3398
|
if (!value) return;
|
|
2323
3399
|
if (import_typescript.default.isStringLiteralLike(value)) return value.text;
|
|
2324
3400
|
if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
|
|
@@ -2334,19 +3410,19 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2334
3410
|
if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
|
|
2335
3411
|
const decl = d.parent.parent;
|
|
2336
3412
|
if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
2337
|
-
const value2 =
|
|
3413
|
+
const value2 = resolve6(decl.initializer, /* @__PURE__ */ new Map());
|
|
2338
3414
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
2339
3415
|
}
|
|
2340
3416
|
}
|
|
2341
3417
|
if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
|
|
2342
|
-
const value2 =
|
|
3418
|
+
const value2 = resolve6(e.expression, /* @__PURE__ */ new Map());
|
|
2343
3419
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
2344
3420
|
}
|
|
2345
|
-
const value =
|
|
3421
|
+
const value = resolve6(e, /* @__PURE__ */ new Map());
|
|
2346
3422
|
return !!value && value !== e && api(value, "useWorkflow");
|
|
2347
3423
|
}
|
|
2348
3424
|
function functionFor(e) {
|
|
2349
|
-
const value =
|
|
3425
|
+
const value = resolve6(e, /* @__PURE__ */ new Map());
|
|
2350
3426
|
if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
|
|
2351
3427
|
return value;
|
|
2352
3428
|
const d = symbol(
|
|
@@ -2369,7 +3445,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2369
3445
|
target.toolDiscovery.warnings.push(warning);
|
|
2370
3446
|
warnings.add(warning);
|
|
2371
3447
|
};
|
|
2372
|
-
const value =
|
|
3448
|
+
const value = resolve6(expression, bindings);
|
|
2373
3449
|
if (!value || seen.has(value)) {
|
|
2374
3450
|
unresolved();
|
|
2375
3451
|
return;
|
|
@@ -2414,8 +3490,8 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2414
3490
|
bindings
|
|
2415
3491
|
);
|
|
2416
3492
|
const schema = property(value, "inputSchema", bindings);
|
|
2417
|
-
const fields =
|
|
2418
|
-
const requiredValue =
|
|
3493
|
+
const fields = resolve6(property(schema, "properties", bindings), bindings);
|
|
3494
|
+
const requiredValue = resolve6(
|
|
2419
3495
|
property(schema, "required", bindings),
|
|
2420
3496
|
bindings
|
|
2421
3497
|
);
|
|
@@ -2504,7 +3580,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2504
3580
|
if (!arg) return;
|
|
2505
3581
|
if (import_typescript.default.isIdentifier(p.name)) {
|
|
2506
3582
|
const s = symbol(p.name);
|
|
2507
|
-
if (s) bound.set(s,
|
|
3583
|
+
if (s) bound.set(s, resolve6(arg, bindings) ?? arg);
|
|
2508
3584
|
} else if (import_typescript.default.isObjectBindingPattern(p.name)) {
|
|
2509
3585
|
for (const b of p.name.elements) {
|
|
2510
3586
|
const value = property(
|
|
@@ -2514,7 +3590,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2514
3590
|
);
|
|
2515
3591
|
const s = symbol(b.name);
|
|
2516
3592
|
if (s && value)
|
|
2517
|
-
bound.set(s,
|
|
3593
|
+
bound.set(s, resolve6(value, bindings) ?? value);
|
|
2518
3594
|
}
|
|
2519
3595
|
}
|
|
2520
3596
|
});
|
|
@@ -2529,7 +3605,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2529
3605
|
for (const source of program.getSourceFiles().filter(local)) {
|
|
2530
3606
|
const walk = (n) => {
|
|
2531
3607
|
if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
|
|
2532
|
-
const obj =
|
|
3608
|
+
const obj = resolve6(n.arguments[0], /* @__PURE__ */ new Map());
|
|
2533
3609
|
const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
|
|
2534
3610
|
if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
|
|
2535
3611
|
definitions.set(id, [...definitions.get(id) ?? [], obj]);
|
|
@@ -2599,24 +3675,24 @@ var readPackageName = async (cwd) => {
|
|
|
2599
3675
|
let current = cwd;
|
|
2600
3676
|
for (; ; ) {
|
|
2601
3677
|
try {
|
|
2602
|
-
const raw = await (0,
|
|
3678
|
+
const raw = await (0, import_promises5.readFile)((0, import_node_path6.join)(current, "package.json"), "utf8");
|
|
2603
3679
|
const parsed = JSON.parse(raw);
|
|
2604
3680
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
|
2605
3681
|
return parsed.name;
|
|
2606
3682
|
}
|
|
2607
3683
|
} catch {
|
|
2608
3684
|
}
|
|
2609
|
-
const parent = (0,
|
|
3685
|
+
const parent = (0, import_node_path6.dirname)(current);
|
|
2610
3686
|
if (parent === current) return void 0;
|
|
2611
3687
|
current = parent;
|
|
2612
3688
|
}
|
|
2613
3689
|
};
|
|
2614
3690
|
var loadEnvFiles = async (cwd) => {
|
|
2615
|
-
for (const filename of [".env", ".env
|
|
2616
|
-
const path = (0,
|
|
3691
|
+
for (const filename of [".env.local", ".env"]) {
|
|
3692
|
+
const path = (0, import_node_path6.join)(cwd, filename);
|
|
2617
3693
|
let raw;
|
|
2618
3694
|
try {
|
|
2619
|
-
raw = await (0,
|
|
3695
|
+
raw = await (0, import_promises5.readFile)(path, "utf8");
|
|
2620
3696
|
} catch {
|
|
2621
3697
|
continue;
|
|
2622
3698
|
}
|
|
@@ -2642,7 +3718,7 @@ var resolveOptions = async (options) => {
|
|
|
2642
3718
|
if (!options.entry) {
|
|
2643
3719
|
throw new Error("Missing required option: --entry <path>.");
|
|
2644
3720
|
}
|
|
2645
|
-
const entry = (0,
|
|
3721
|
+
const entry = (0, import_node_path6.isAbsolute)(options.entry) ? options.entry : (0, import_node_path6.resolve)(options.cwd, options.entry);
|
|
2646
3722
|
const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
|
|
2647
3723
|
const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
|
|
2648
3724
|
return {
|
|
@@ -2656,19 +3732,19 @@ var resolveOptions = async (options) => {
|
|
|
2656
3732
|
};
|
|
2657
3733
|
};
|
|
2658
3734
|
var createFrameworkMarkerStubs = async () => {
|
|
2659
|
-
const root = await (0,
|
|
3735
|
+
const root = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
|
|
2660
3736
|
for (const moduleName of ["server-only", "client-only"]) {
|
|
2661
|
-
const directory = (0,
|
|
2662
|
-
await (0,
|
|
2663
|
-
await (0,
|
|
2664
|
-
await (0,
|
|
2665
|
-
(0,
|
|
3737
|
+
const directory = (0, import_node_path6.join)(root, moduleName);
|
|
3738
|
+
await (0, import_promises5.mkdir)(directory, { recursive: true });
|
|
3739
|
+
await (0, import_promises5.writeFile)((0, import_node_path6.join)(directory, "index.js"), "", "utf8");
|
|
3740
|
+
await (0, import_promises5.writeFile)(
|
|
3741
|
+
(0, import_node_path6.join)(directory, "package.json"),
|
|
2666
3742
|
JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
|
|
2667
3743
|
"utf8"
|
|
2668
3744
|
);
|
|
2669
3745
|
}
|
|
2670
3746
|
const previousNodePath = process.env.NODE_PATH;
|
|
2671
|
-
process.env.NODE_PATH = previousNodePath ? `${root}${
|
|
3747
|
+
process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path6.delimiter}${previousNodePath}` : root;
|
|
2672
3748
|
const moduleWithPrivateInit = import_node_module.default;
|
|
2673
3749
|
moduleWithPrivateInit._initPaths?.();
|
|
2674
3750
|
return {
|
|
@@ -2676,48 +3752,71 @@ var createFrameworkMarkerStubs = async () => {
|
|
|
2676
3752
|
if (previousNodePath === void 0) delete process.env.NODE_PATH;
|
|
2677
3753
|
else process.env.NODE_PATH = previousNodePath;
|
|
2678
3754
|
moduleWithPrivateInit._initPaths?.();
|
|
2679
|
-
await (0,
|
|
3755
|
+
await (0, import_promises5.rm)(root, { recursive: true, force: true });
|
|
2680
3756
|
}
|
|
2681
3757
|
};
|
|
2682
3758
|
};
|
|
2683
|
-
var
|
|
3759
|
+
var openEntry = async (entry) => {
|
|
2684
3760
|
const stubs = await createFrameworkMarkerStubs();
|
|
2685
3761
|
let unregisterPaths;
|
|
3762
|
+
let unregisterEsm;
|
|
3763
|
+
const unregisterTsx = (0, import_api.register)();
|
|
3764
|
+
const release = async () => {
|
|
3765
|
+
unregisterPaths?.();
|
|
3766
|
+
unregisterTsx();
|
|
3767
|
+
await unregisterEsm?.();
|
|
3768
|
+
await stubs.cleanup();
|
|
3769
|
+
};
|
|
2686
3770
|
try {
|
|
2687
|
-
const tsconfig = await findNearestFile((0,
|
|
3771
|
+
const tsconfig = await findNearestFile((0, import_node_path6.dirname)(entry), "tsconfig.json");
|
|
3772
|
+
unregisterEsm = (0, import_api2.register)(tsconfig ? { tsconfig } : {});
|
|
2688
3773
|
unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
|
|
2689
|
-
const module2 = (0,
|
|
3774
|
+
const module2 = (0, import_node_module.createRequire)((0, import_node_path6.join)((0, import_node_path6.dirname)(entry), "kortyx-cli.cjs"))(entry);
|
|
2690
3775
|
if (!isRecord(module2)) {
|
|
2691
3776
|
throw new Error(`Entry module did not export an object: ${entry}`);
|
|
2692
3777
|
}
|
|
2693
|
-
return module2;
|
|
3778
|
+
return { module: module2, release };
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
await release();
|
|
3781
|
+
throw error;
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
var importEntry = async (entry) => {
|
|
3785
|
+
const loaded = await openEntry(entry);
|
|
3786
|
+
try {
|
|
3787
|
+
return loaded.module;
|
|
2694
3788
|
} finally {
|
|
2695
|
-
|
|
2696
|
-
await stubs.cleanup();
|
|
3789
|
+
await loaded.release();
|
|
2697
3790
|
}
|
|
2698
3791
|
};
|
|
2699
3792
|
var findNearestFile = async (startDirectory, filename) => {
|
|
2700
3793
|
let current = startDirectory;
|
|
2701
3794
|
for (; ; ) {
|
|
2702
|
-
const candidate = (0,
|
|
3795
|
+
const candidate = (0, import_node_path6.join)(current, filename);
|
|
2703
3796
|
try {
|
|
2704
|
-
await (0,
|
|
3797
|
+
await (0, import_promises5.access)(candidate);
|
|
2705
3798
|
return candidate;
|
|
2706
3799
|
} catch {
|
|
2707
|
-
const parent = (0,
|
|
3800
|
+
const parent = (0, import_node_path6.dirname)(current);
|
|
2708
3801
|
if (parent === current) return void 0;
|
|
2709
3802
|
current = parent;
|
|
2710
3803
|
}
|
|
2711
3804
|
}
|
|
2712
3805
|
};
|
|
2713
3806
|
var registerTsconfigPaths = async (tsconfigPath) => {
|
|
2714
|
-
const
|
|
3807
|
+
const parsed = import_typescript2.default.parseConfigFileTextToJson(
|
|
3808
|
+
tsconfigPath,
|
|
3809
|
+
await (0, import_promises5.readFile)(tsconfigPath, "utf8")
|
|
3810
|
+
);
|
|
3811
|
+
if (parsed.error)
|
|
3812
|
+
throw new Error(`Invalid TypeScript configuration: ${tsconfigPath}`);
|
|
3813
|
+
const raw = parsed.config;
|
|
2715
3814
|
if (!isRecord(raw)) return () => void 0;
|
|
2716
3815
|
const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
|
|
2717
3816
|
const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
|
|
2718
3817
|
if (!paths) return () => void 0;
|
|
2719
|
-
const tsconfigDirectory = (0,
|
|
2720
|
-
const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0,
|
|
3818
|
+
const tsconfigDirectory = (0, import_node_path6.dirname)(tsconfigPath);
|
|
3819
|
+
const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path6.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
|
|
2721
3820
|
const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
|
|
2722
3821
|
if (!Array.isArray(rawTargets)) return [];
|
|
2723
3822
|
return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
|
|
@@ -2746,7 +3845,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
|
|
|
2746
3845
|
request.length - mapping.patternSuffix.length
|
|
2747
3846
|
) : "";
|
|
2748
3847
|
const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
|
|
2749
|
-
for (const candidate of candidateModulePaths((0,
|
|
3848
|
+
for (const candidate of candidateModulePaths((0, import_node_path6.resolve)(baseUrl, mapped))) {
|
|
2750
3849
|
try {
|
|
2751
3850
|
return originalResolve.call(this, candidate, parent, isMain, options);
|
|
2752
3851
|
} catch {
|
|
@@ -2767,10 +3866,10 @@ var candidateModulePaths = (basePath) => [
|
|
|
2767
3866
|
`${basePath}.cts`,
|
|
2768
3867
|
`${basePath}.js`,
|
|
2769
3868
|
`${basePath}.jsx`,
|
|
2770
|
-
(0,
|
|
2771
|
-
(0,
|
|
2772
|
-
(0,
|
|
2773
|
-
(0,
|
|
3869
|
+
(0, import_node_path6.join)(basePath, "index.ts"),
|
|
3870
|
+
(0, import_node_path6.join)(basePath, "index.tsx"),
|
|
3871
|
+
(0, import_node_path6.join)(basePath, "index.js"),
|
|
3872
|
+
(0, import_node_path6.join)(basePath, "index.jsx")
|
|
2774
3873
|
];
|
|
2775
3874
|
var valueFromModule = (module2, exportName) => {
|
|
2776
3875
|
if (exportName) {
|
|
@@ -2800,7 +3899,7 @@ var projectSnapshots = async (value, options) => {
|
|
|
2800
3899
|
}
|
|
2801
3900
|
const knownWorkflowIds = workflows.map((workflow) => workflow.id);
|
|
2802
3901
|
return workflows.map(
|
|
2803
|
-
(workflow) => (0,
|
|
3902
|
+
(workflow) => (0, import_agent2.projectWorkflowTopology)({
|
|
2804
3903
|
workflow,
|
|
2805
3904
|
environment: options.environment,
|
|
2806
3905
|
service: options.service,
|
|
@@ -2928,12 +4027,30 @@ var runTopologyPush = async (rawOptions) => {
|
|
|
2928
4027
|
printResults(results, options);
|
|
2929
4028
|
};
|
|
2930
4029
|
var createCliProgram = () => {
|
|
2931
|
-
const program = new
|
|
4030
|
+
const program = new import_commander6.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
|
|
2932
4031
|
"after",
|
|
2933
4032
|
'\nRun "kortyx <command> --help" for detailed command options.'
|
|
2934
4033
|
);
|
|
2935
4034
|
program.addCommand(createStudioCommand());
|
|
2936
4035
|
program.addCommand(createConnectionsCommand());
|
|
4036
|
+
let releaseEvalEntry;
|
|
4037
|
+
program.addCommand(
|
|
4038
|
+
createLocalEvalsCommand({
|
|
4039
|
+
load: async (entry, envFiles) => {
|
|
4040
|
+
Object.assign(
|
|
4041
|
+
process.env,
|
|
4042
|
+
await loadEvalEnvironment(process.cwd(), envFiles)
|
|
4043
|
+
);
|
|
4044
|
+
const loaded = await openEntry(entry);
|
|
4045
|
+
releaseEvalEntry = loaded.release;
|
|
4046
|
+
return loaded.module;
|
|
4047
|
+
},
|
|
4048
|
+
release: async () => {
|
|
4049
|
+
await releaseEvalEntry?.();
|
|
4050
|
+
releaseEvalEntry = void 0;
|
|
4051
|
+
}
|
|
4052
|
+
})
|
|
4053
|
+
);
|
|
2937
4054
|
const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
|
|
2938
4055
|
topology.command("push").description("Project and publish workflow topology.").requiredOption(
|
|
2939
4056
|
"--entry <path>",
|
|
@@ -2971,7 +4088,7 @@ var main = async () => {
|
|
|
2971
4088
|
try {
|
|
2972
4089
|
await program.parseAsync(process.argv);
|
|
2973
4090
|
} catch (error) {
|
|
2974
|
-
if (error instanceof
|
|
4091
|
+
if (error instanceof import_commander6.CommanderError && error.exitCode === 0) return;
|
|
2975
4092
|
if (error instanceof StudioReadError) {
|
|
2976
4093
|
console.error(
|
|
2977
4094
|
process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`
|