@kortyx/cli 0.11.3 → 0.12.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 +25 -0
- package/README.md +118 -0
- package/dist/index.js +1189 -263
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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:
|
|
@@ -265,6 +275,12 @@ services:
|
|
|
265
275
|
NODE_ENV: production
|
|
266
276
|
API_HOST: 0.0.0.0
|
|
267
277
|
API_PORT: 6400
|
|
278
|
+
KORTYX_EVAL_JUDGE_MODEL: \${KORTYX_EVAL_JUDGE_MODEL:-}
|
|
279
|
+
KORTYX_EVAL_JUDGE_API_KEY: \${KORTYX_EVAL_JUDGE_API_KEY:-}
|
|
280
|
+
KORTYX_EVAL_JUDGE_API: \${KORTYX_EVAL_JUDGE_API:-responses}
|
|
281
|
+
KORTYX_EVAL_JUDGE_BASE_URL: \${KORTYX_EVAL_JUDGE_BASE_URL:-}
|
|
282
|
+
KORTYX_EVAL_JUDGE_ID: \${KORTYX_EVAL_JUDGE_ID:-}
|
|
283
|
+
KORTYX_EVAL_JUDGE_VERSION: \${KORTYX_EVAL_JUDGE_VERSION:-}
|
|
268
284
|
KORTYX_STUDIO_RELEASE: \${KORTYX_STUDIO_IMAGE_TAG:-latest}
|
|
269
285
|
ports:
|
|
270
286
|
- "127.0.0.1:\${API_PORT:-6400}:6400"
|
|
@@ -838,48 +854,1028 @@ var createConnectionsCommand = (log = console.log, request = fetch) => {
|
|
|
838
854
|
print({ connection: profile, verified: true }, options);
|
|
839
855
|
}
|
|
840
856
|
);
|
|
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)) {
|
|
857
|
+
common(
|
|
858
|
+
command.command("use <name>").description(
|
|
859
|
+
"Select the default connection; agents should prefer per-command --connection."
|
|
860
|
+
)
|
|
861
|
+
).action(async (name, options) => {
|
|
862
|
+
const config = await readConnections(options.configHome);
|
|
863
|
+
if (name === "local" ? !await readStudioConfig(options.home) : !config.profiles.some((profile) => profile.name === name)) {
|
|
864
|
+
throw new StudioReadError(
|
|
865
|
+
"unknown_connection",
|
|
866
|
+
"Connection not found. Run kortyx connections list."
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
config.current = name;
|
|
870
|
+
await saveConnections(config, options.configHome);
|
|
871
|
+
print({ current: name }, options);
|
|
872
|
+
});
|
|
873
|
+
common(
|
|
874
|
+
command.command("remove <name>").description(
|
|
875
|
+
"Remove a saved locator, not its credential or remote project."
|
|
876
|
+
)
|
|
877
|
+
).action(async (name, options) => {
|
|
878
|
+
const config = await readConnections(options.configHome);
|
|
879
|
+
if (!config.profiles.some((profile) => profile.name === name))
|
|
880
|
+
throw new StudioReadError(
|
|
881
|
+
"unknown_connection",
|
|
882
|
+
"Saved connection not found; managed local cannot be removed."
|
|
883
|
+
);
|
|
884
|
+
config.profiles = config.profiles.filter(
|
|
885
|
+
(profile) => profile.name !== name
|
|
886
|
+
);
|
|
887
|
+
if (config.current === name) delete config.current;
|
|
888
|
+
await saveConnections(config, options.configHome);
|
|
889
|
+
print({ removed: name }, options);
|
|
890
|
+
});
|
|
891
|
+
return command;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// src/evals/command.ts
|
|
895
|
+
var import_node_path3 = require("path");
|
|
896
|
+
var import_agent = require("@kortyx/agent");
|
|
897
|
+
var import_commander2 = require("commander");
|
|
898
|
+
|
|
899
|
+
// src/evals/reporter.ts
|
|
900
|
+
var text = (value) => value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trim();
|
|
901
|
+
var duration = (ms) => ms < 1e3 ? `${Math.round(ms)} ms` : `${(ms / 1e3).toFixed(2)} s`;
|
|
902
|
+
var wide = /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u;
|
|
903
|
+
var fit = (value, columns) => {
|
|
904
|
+
const clean = value.replace(/[\p{Cc}\p{Cf}]/gu, " ").trimEnd();
|
|
905
|
+
let used = 0;
|
|
906
|
+
let result = "";
|
|
907
|
+
for (const char of clean) {
|
|
908
|
+
used += /\p{Mark}/u.test(char) ? 0 : wide.test(char) ? 2 : 1;
|
|
909
|
+
if (used > columns - 2) return `${result}\u2026`;
|
|
910
|
+
result += char;
|
|
911
|
+
}
|
|
912
|
+
return result;
|
|
913
|
+
};
|
|
914
|
+
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
915
|
+
var phaseNames = {
|
|
916
|
+
params: "Preparing inputs",
|
|
917
|
+
setup: "Setting up test",
|
|
918
|
+
execute: "Running workflow",
|
|
919
|
+
responder: "Resolving human input",
|
|
920
|
+
reference: "Loading reference",
|
|
921
|
+
grading: "Checking expectations",
|
|
922
|
+
cleanup: "Cleaning up",
|
|
923
|
+
reporting: "Reporting results"
|
|
924
|
+
};
|
|
925
|
+
function createEvalTerminalReporter(write, color, options = {}) {
|
|
926
|
+
const now = options.now ?? Date.now;
|
|
927
|
+
const columns = () => Math.max(12, options.columns?.() ?? process.stdout.columns ?? 100);
|
|
928
|
+
const paint = (code, value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
929
|
+
const active = /* @__PURE__ */ new Map();
|
|
930
|
+
let names = /* @__PURE__ */ new Map();
|
|
931
|
+
let steps = /* @__PURE__ */ new Map();
|
|
932
|
+
let completed = 0;
|
|
933
|
+
let total = 0;
|
|
934
|
+
let started = 0;
|
|
935
|
+
let frame = 0;
|
|
936
|
+
let liveLines = 0;
|
|
937
|
+
let paused = false;
|
|
938
|
+
let running = false;
|
|
939
|
+
let timer;
|
|
940
|
+
const key = (id, repetition) => JSON.stringify([id, repetition]);
|
|
941
|
+
const clear = () => {
|
|
942
|
+
if (liveLines) write(`\x1B[${liveLines}A\r\x1B[0J`);
|
|
943
|
+
liveLines = 0;
|
|
944
|
+
};
|
|
945
|
+
const line = (value = "") => {
|
|
946
|
+
clear();
|
|
947
|
+
write(`${value}
|
|
948
|
+
`);
|
|
949
|
+
};
|
|
950
|
+
const progressBar = (done, count, width, pulse = false) => {
|
|
951
|
+
const filled = count ? Math.min(width, Math.floor(done / count * width)) : 0;
|
|
952
|
+
const remaining = width - filled;
|
|
953
|
+
const pending = pulse && remaining > 0 ? Array.from(
|
|
954
|
+
{ length: remaining },
|
|
955
|
+
(_, index) => index === frame % remaining ? "\u2578" : "\u2591"
|
|
956
|
+
).join("") : "\u2591".repeat(remaining);
|
|
957
|
+
return `${"\u2501".repeat(filled)}${pending}`;
|
|
958
|
+
};
|
|
959
|
+
const render = () => {
|
|
960
|
+
if (!color || !running || paused) return;
|
|
961
|
+
clear();
|
|
962
|
+
const width = columns();
|
|
963
|
+
const barWidth = Math.max(6, Math.min(22, width - 60));
|
|
964
|
+
const percent = total ? Math.floor(completed / total * 100) : 0;
|
|
965
|
+
const lines = [
|
|
966
|
+
paint(
|
|
967
|
+
36,
|
|
968
|
+
fit(
|
|
969
|
+
` RUN ${progressBar(completed, total, barWidth)} ${completed}/${total || "?"} \xB7 ${percent}%`,
|
|
970
|
+
width
|
|
971
|
+
)
|
|
972
|
+
),
|
|
973
|
+
paint(
|
|
974
|
+
90,
|
|
975
|
+
fit(
|
|
976
|
+
` ${active.size} running \xB7 ${Math.max(0, total - completed - active.size)} queued \xB7 ${((now() - started) / 1e3).toFixed(1)}s elapsed`,
|
|
977
|
+
width
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
];
|
|
981
|
+
for (const item of [...active.values()].slice(0, 6)) {
|
|
982
|
+
const name = names.get(item.caseId) ?? item.caseId;
|
|
983
|
+
lines.push(
|
|
984
|
+
paint(
|
|
985
|
+
33,
|
|
986
|
+
fit(
|
|
987
|
+
` ${frames[frame % frames.length]} ${name} #${item.repetition}`,
|
|
988
|
+
width
|
|
989
|
+
)
|
|
990
|
+
)
|
|
991
|
+
);
|
|
992
|
+
const stepCount = steps.get(item.caseId) ?? 1;
|
|
993
|
+
const step = item.stepIndex === void 0 ? "" : `Step ${item.stepIndex + 1}/${stepCount} \xB7 `;
|
|
994
|
+
lines.push(
|
|
995
|
+
paint(
|
|
996
|
+
item.phase === "grading" ? 35 : 90,
|
|
997
|
+
fit(
|
|
998
|
+
` ${progressBar(item.completedSteps, stepCount, 6, true)} ${step}${phaseNames[item.phase]} \xB7 ${((now() - item.started) / 1e3).toFixed(1)}s`,
|
|
999
|
+
width
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (active.size > 6)
|
|
1005
|
+
lines.push(paint(90, ` +${active.size - 6} more running`));
|
|
1006
|
+
write(`${lines.join("\n")}
|
|
1007
|
+
`);
|
|
1008
|
+
liveLines = lines.length;
|
|
1009
|
+
frame++;
|
|
1010
|
+
};
|
|
1011
|
+
const stop = () => {
|
|
1012
|
+
if (timer) clearInterval(timer);
|
|
1013
|
+
timer = void 0;
|
|
1014
|
+
running = false;
|
|
1015
|
+
clear();
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
start(suite) {
|
|
1019
|
+
stop();
|
|
1020
|
+
names = new Map(
|
|
1021
|
+
suite.cases.map((item) => [item.id, item.name ?? item.id])
|
|
1022
|
+
);
|
|
1023
|
+
steps = new Map(suite.cases.map((item) => [item.id, item.steps.length]));
|
|
1024
|
+
active.clear();
|
|
1025
|
+
completed = total = frame = 0;
|
|
1026
|
+
paused = false;
|
|
1027
|
+
started = now();
|
|
1028
|
+
line();
|
|
1029
|
+
if (color)
|
|
1030
|
+
line(paint(35, " \u2501".padEnd(Math.min(columns() - 1, 64), "\u2501")));
|
|
1031
|
+
line(paint("1;35", `\u25C6 Kortyx Evals \xB7 ${text(suite.name ?? suite.id)}`));
|
|
1032
|
+
line(paint(90, ` ${text(suite.id)} \xB7 local execution`));
|
|
1033
|
+
line();
|
|
1034
|
+
running = true;
|
|
1035
|
+
if (color) {
|
|
1036
|
+
timer = setInterval(render, 100);
|
|
1037
|
+
timer.unref();
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
progress(event) {
|
|
1041
|
+
if (event.type === "run-started") {
|
|
1042
|
+
total = event.caseIds.length * event.repetitions;
|
|
1043
|
+
line(
|
|
1044
|
+
paint(
|
|
1045
|
+
90,
|
|
1046
|
+
` ${event.caseIds.length} ${event.caseIds.length === 1 ? "test" : "tests"} \xB7 ${total} ${total === 1 ? "attempt" : "attempts"} \xB7 concurrency ${event.concurrency}`
|
|
1047
|
+
)
|
|
1048
|
+
);
|
|
1049
|
+
line();
|
|
1050
|
+
} else if (event.type === "case-started") {
|
|
1051
|
+
active.set(key(event.caseId, event.repetition), {
|
|
1052
|
+
caseId: event.caseId,
|
|
1053
|
+
repetition: event.repetition,
|
|
1054
|
+
started: now(),
|
|
1055
|
+
phase: "params",
|
|
1056
|
+
completedSteps: 0
|
|
1057
|
+
});
|
|
1058
|
+
if (!color)
|
|
1059
|
+
line(
|
|
1060
|
+
` \u2192 ${text(names.get(event.caseId) ?? event.caseId)} [${text(event.caseId)}] #${event.repetition} RUNNING`
|
|
1061
|
+
);
|
|
1062
|
+
} else if (event.type === "case-progress") {
|
|
1063
|
+
const item = active.get(key(event.caseId, event.repetition));
|
|
1064
|
+
if (item) {
|
|
1065
|
+
item.phase = event.phase;
|
|
1066
|
+
item.stepIndex = event.stepIndex;
|
|
1067
|
+
}
|
|
1068
|
+
} else if (event.type === "step-completed") {
|
|
1069
|
+
const item = active.get(key(event.caseId, event.repetition));
|
|
1070
|
+
if (item) item.completedSteps = event.step.index + 1;
|
|
1071
|
+
const passed = event.step.status === "passed";
|
|
1072
|
+
const criterionCount = event.step.criteria.length;
|
|
1073
|
+
const detail = criterionCount ? ` \xB7 ${event.step.criteria.filter((criterion) => criterion.passed).length}/${criterionCount} criteria passed` : "";
|
|
1074
|
+
line(
|
|
1075
|
+
` ${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}`
|
|
1076
|
+
);
|
|
1077
|
+
} else {
|
|
1078
|
+
const item = event.result;
|
|
1079
|
+
active.delete(key(item.caseId, item.repetition));
|
|
1080
|
+
completed++;
|
|
1081
|
+
const passed = item.status === "passed";
|
|
1082
|
+
const cancelled = item.status === "cancelled";
|
|
1083
|
+
const code = passed ? 32 : cancelled || item.status === "ungraded" ? 33 : 31;
|
|
1084
|
+
const symbol = passed ? "\u2713" : cancelled ? "\u2212" : item.status === "ungraded" ? "\u25C7" : "\u2717";
|
|
1085
|
+
line(
|
|
1086
|
+
` ${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())}`
|
|
1087
|
+
);
|
|
1088
|
+
line();
|
|
1089
|
+
}
|
|
1090
|
+
render();
|
|
1091
|
+
},
|
|
1092
|
+
finish(result) {
|
|
1093
|
+
stop();
|
|
1094
|
+
for (const item of result.cases) {
|
|
1095
|
+
for (const step of item.steps) {
|
|
1096
|
+
if (step.status === "passed") continue;
|
|
1097
|
+
line();
|
|
1098
|
+
line(
|
|
1099
|
+
paint(
|
|
1100
|
+
31,
|
|
1101
|
+
` ${text(names.get(item.caseId) ?? item.caseId)} \u203A attempt ${item.repetition} \u203A step ${step.index + 1}`
|
|
1102
|
+
)
|
|
1103
|
+
);
|
|
1104
|
+
line(
|
|
1105
|
+
` Expected ${step.expectation.type} \xB7 observed ${step.observation.type}`
|
|
1106
|
+
);
|
|
1107
|
+
if (step.reason) line(` ${text(step.reason)}`);
|
|
1108
|
+
for (const criterion of step.criteria.filter(
|
|
1109
|
+
(value) => !value.passed
|
|
1110
|
+
)) {
|
|
1111
|
+
line(` \u2717 ${text(criterion.text)}`);
|
|
1112
|
+
line(` ${text(criterion.reason)}`);
|
|
1113
|
+
for (const evidence of criterion.evidence)
|
|
1114
|
+
line(paint(90, ` Evidence: ${text(evidence)}`));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
for (const issue of item.errors)
|
|
1118
|
+
line(
|
|
1119
|
+
` ${text(item.caseId)} \xB7 ${issue.phase} [${issue.code}] ${text(issue.message)}`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
for (const issue of result.errors)
|
|
1123
|
+
line(` ${issue.phase} [${issue.code}] ${text(issue.message)}`);
|
|
1124
|
+
const successful = result.status === "passed";
|
|
1125
|
+
const code = successful ? 32 : result.status === "cancelled" || result.status === "ungraded" ? 33 : 31;
|
|
1126
|
+
line();
|
|
1127
|
+
if (color) {
|
|
1128
|
+
line(
|
|
1129
|
+
paint(
|
|
1130
|
+
code,
|
|
1131
|
+
` ${progressBar(result.cases.length, result.cases.length, Math.min(22, columns() - 3))} ${result.cases.length}/${result.cases.length}`
|
|
1132
|
+
)
|
|
1133
|
+
);
|
|
1134
|
+
line(
|
|
1135
|
+
paint(
|
|
1136
|
+
`${code};1`,
|
|
1137
|
+
` ${successful ? "\u2713 SUITE PASSED" : `\u25C7 SUITE ${result.status.toUpperCase()}`}`
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
line(
|
|
1142
|
+
` ${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`)}` : ""}`
|
|
1143
|
+
);
|
|
1144
|
+
line(
|
|
1145
|
+
paint(
|
|
1146
|
+
90,
|
|
1147
|
+
` Duration ${duration(result.durationMs)} \xB7 ${result.cases.length} ${result.cases.length === 1 ? "attempt" : "attempts"}`
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
line();
|
|
1151
|
+
},
|
|
1152
|
+
pause() {
|
|
1153
|
+
paused = true;
|
|
1154
|
+
clear();
|
|
1155
|
+
},
|
|
1156
|
+
resume() {
|
|
1157
|
+
paused = false;
|
|
1158
|
+
render();
|
|
1159
|
+
},
|
|
1160
|
+
close: stop
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/evals/command.ts
|
|
1165
|
+
var integer = (value) => {
|
|
1166
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
|
|
1167
|
+
throw new import_commander2.InvalidArgumentError("Use a positive integer.");
|
|
1168
|
+
return Number(value);
|
|
1169
|
+
};
|
|
1170
|
+
var isRunner = (value) => value !== null && typeof value === "object" && "run" in value && typeof value.run === "function" && "describe" in value && typeof value.describe === "function";
|
|
1171
|
+
function createLocalEvalsCommand(runtime) {
|
|
1172
|
+
const write = runtime.write ?? process.stdout.write.bind(process.stdout);
|
|
1173
|
+
const exitCode = runtime.setExitCode ?? ((value) => {
|
|
1174
|
+
process.exitCode = value;
|
|
1175
|
+
});
|
|
1176
|
+
const color = runtime.color ?? Boolean(
|
|
1177
|
+
process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0"
|
|
1178
|
+
);
|
|
1179
|
+
const load = async (options) => {
|
|
1180
|
+
const module2 = await runtime.load(
|
|
1181
|
+
(0, import_node_path3.resolve)(runtime.cwd ?? process.cwd(), options.entry),
|
|
1182
|
+
options.env
|
|
1183
|
+
);
|
|
1184
|
+
const selected = options.export ? module2[options.export] : module2.evals ?? module2.default;
|
|
1185
|
+
if (!isRunner(selected))
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Export ${options.export ?? '"evals" or default'} must be a createEvals instance.`
|
|
1188
|
+
);
|
|
1189
|
+
const suites = selected.describe().suites.map((suite) => (0, import_agent.parseEvalSuite)(suite));
|
|
1190
|
+
if (!suites.length) throw new Error("No eval suites are configured.");
|
|
1191
|
+
return { runner: selected, suites };
|
|
1192
|
+
};
|
|
1193
|
+
const withRunner = async (options, action) => {
|
|
1194
|
+
try {
|
|
1195
|
+
await action(await load(options));
|
|
1196
|
+
} finally {
|
|
1197
|
+
await runtime.release?.();
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const entryOptions = (command) => command.requiredOption(
|
|
1201
|
+
"--entry <path>",
|
|
1202
|
+
"Module exporting your createEvals instance (TypeScript or JavaScript)."
|
|
1203
|
+
).option("--export <name>", "Named export; defaults to evals or default.").option(
|
|
1204
|
+
"--env <path>",
|
|
1205
|
+
"Load an env file before the entry; repeat for overlays. Replaces automatic env loading.",
|
|
1206
|
+
(value, previous) => [...previous, value],
|
|
1207
|
+
[]
|
|
1208
|
+
).option(
|
|
1209
|
+
"--json",
|
|
1210
|
+
"Print machine-readable JSON instead of the terminal report."
|
|
1211
|
+
);
|
|
1212
|
+
const root = new import_commander2.Command("evals").description(
|
|
1213
|
+
"Run application evals locally, without Studio."
|
|
1214
|
+
);
|
|
1215
|
+
entryOptions(
|
|
1216
|
+
root.command("list").description("List suites exported by an application module.")
|
|
1217
|
+
).action(
|
|
1218
|
+
(options) => withRunner(options, async ({ suites }) => {
|
|
1219
|
+
write(
|
|
1220
|
+
options.json ? `${JSON.stringify(suites)}
|
|
1221
|
+
` : suites.map(
|
|
1222
|
+
(suite) => `${suite.id} ${suite.name ?? suite.id} ${suite.cases.length} cases
|
|
1223
|
+
`
|
|
1224
|
+
).join("")
|
|
1225
|
+
);
|
|
1226
|
+
})
|
|
1227
|
+
);
|
|
1228
|
+
entryOptions(
|
|
1229
|
+
root.command("run").description(
|
|
1230
|
+
"Execute suites and report progress, failures and results locally."
|
|
1231
|
+
)
|
|
1232
|
+
).option(
|
|
1233
|
+
"--suite <id>",
|
|
1234
|
+
"Run one suite; omitted runs all suites in definition order."
|
|
1235
|
+
).option(
|
|
1236
|
+
"--case <id...>",
|
|
1237
|
+
"Select cases within --suite (space-separated IDs)."
|
|
1238
|
+
).option("--repetitions <count>", "Override attempts per case.", integer).option(
|
|
1239
|
+
"--concurrency <count>",
|
|
1240
|
+
"Override concurrent cases within each suite.",
|
|
1241
|
+
integer
|
|
1242
|
+
).option("--no-color", "Disable terminal colors and live progress.").option("--color", "Force terminal colors and live progress.").action(
|
|
1243
|
+
(options, command) => withRunner(options, async ({ runner, suites }) => {
|
|
1244
|
+
const selected = options.suite ? suites.filter((suite) => suite.id === options.suite) : suites;
|
|
1245
|
+
if (!selected.length)
|
|
1246
|
+
throw new Error(`Unknown eval suite: ${options.suite}`);
|
|
1247
|
+
if (options.case && (!options.suite || new Set(options.case).size !== options.case.length || options.case.some(
|
|
1248
|
+
(id) => !selected[0]?.cases.some((item) => item.id === id)
|
|
1249
|
+
)))
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
"--case requires --suite and unique existing case IDs."
|
|
1252
|
+
);
|
|
1253
|
+
const controller = new AbortController();
|
|
1254
|
+
const removeInterrupt = (runtime.onInterrupt ?? ((abort) => {
|
|
1255
|
+
process.once("SIGINT", abort);
|
|
1256
|
+
return () => {
|
|
1257
|
+
process.removeListener("SIGINT", abort);
|
|
1258
|
+
};
|
|
1259
|
+
}))(() => controller.abort());
|
|
1260
|
+
const interactive = options.color && !options.json && (color || command.getOptionValueSource("color") === "cli");
|
|
1261
|
+
const reporter = createEvalTerminalReporter(write, interactive);
|
|
1262
|
+
const restoreOutput = [];
|
|
1263
|
+
if (!runtime.write && interactive) {
|
|
1264
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
1265
|
+
const original = stream.write;
|
|
1266
|
+
const wrapped = new Proxy(original, {
|
|
1267
|
+
apply(target, receiver, args) {
|
|
1268
|
+
reporter.pause();
|
|
1269
|
+
const written = Reflect.apply(target, receiver, args);
|
|
1270
|
+
if (String(args[0]).endsWith("\n")) reporter.resume();
|
|
1271
|
+
return written;
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
stream.write = wrapped;
|
|
1275
|
+
restoreOutput.push(() => {
|
|
1276
|
+
if (stream.write === wrapped) stream.write = original;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const runs = [];
|
|
1281
|
+
try {
|
|
1282
|
+
for (const suite of selected) {
|
|
1283
|
+
if (controller.signal.aborted) break;
|
|
1284
|
+
if (!options.json) reporter.start(suite);
|
|
1285
|
+
const result = await runner.run({
|
|
1286
|
+
suiteId: suite.id,
|
|
1287
|
+
signal: controller.signal,
|
|
1288
|
+
includeActivity: !options.json,
|
|
1289
|
+
...options.case ? { caseIds: options.case } : {},
|
|
1290
|
+
...options.repetitions !== void 0 ? { repetitions: options.repetitions } : {},
|
|
1291
|
+
...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
|
|
1292
|
+
...!options.json ? { onProgress: reporter.progress } : {}
|
|
1293
|
+
});
|
|
1294
|
+
runs.push(result);
|
|
1295
|
+
if (!options.json) reporter.finish(result);
|
|
1296
|
+
}
|
|
1297
|
+
const counts = { passed: 0, failed: 0, error: 0, cancelled: 0 };
|
|
1298
|
+
for (const run2 of runs)
|
|
1299
|
+
for (const key of Object.keys(counts))
|
|
1300
|
+
counts[key] += run2.counts[key];
|
|
1301
|
+
const cancelled = controller.signal.aborted || counts.cancelled > 0;
|
|
1302
|
+
const passed = runs.length === selected.length && runs.every((run2) => run2.status === "passed");
|
|
1303
|
+
const status = cancelled ? "cancelled" : counts.error || runs.some((run2) => run2.status === "error") ? "error" : passed ? "passed" : "failed";
|
|
1304
|
+
if (options.json)
|
|
1305
|
+
write(
|
|
1306
|
+
`${JSON.stringify({ schemaVersion: 1, status, counts, runs })}
|
|
1307
|
+
`
|
|
1308
|
+
);
|
|
1309
|
+
else if (selected.length > 1)
|
|
1310
|
+
write(
|
|
1311
|
+
`Overall ${status.toUpperCase()} \xB7 ${runs.length}/${selected.length} suites \xB7 ${counts.passed} passed \xB7 ${counts.failed} failed \xB7 ${counts.error} errors
|
|
1312
|
+
`
|
|
1313
|
+
);
|
|
1314
|
+
exitCode(cancelled ? 130 : passed ? 0 : 1);
|
|
1315
|
+
} finally {
|
|
1316
|
+
reporter.close();
|
|
1317
|
+
for (const restore of restoreOutput) restore();
|
|
1318
|
+
removeInterrupt();
|
|
1319
|
+
}
|
|
1320
|
+
})
|
|
1321
|
+
);
|
|
1322
|
+
return root;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/evals/environment.ts
|
|
1326
|
+
var import_promises3 = require("fs/promises");
|
|
1327
|
+
var import_node_path4 = require("path");
|
|
1328
|
+
var import_node_util = require("util");
|
|
1329
|
+
var exists = async (file) => {
|
|
1330
|
+
try {
|
|
1331
|
+
await (0, import_promises3.lstat)(file);
|
|
1332
|
+
return true;
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (error.code === "ENOENT") return false;
|
|
1335
|
+
throw new Error(`Cannot read eval configuration: ${file}`);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var findConfiguration = async (cwd) => {
|
|
1339
|
+
let directory = (0, import_node_path4.resolve)(cwd);
|
|
1340
|
+
const directories = [];
|
|
1341
|
+
for (; ; ) {
|
|
1342
|
+
directories.unshift(directory);
|
|
1343
|
+
const profile = (0, import_node_path4.join)(directory, ".env.evals.json");
|
|
1344
|
+
if (await exists(profile)) return { profile, directories };
|
|
1345
|
+
if (await exists((0, import_node_path4.join)(directory, ".git")) || await exists((0, import_node_path4.join)(directory, "pnpm-workspace.yaml")))
|
|
1346
|
+
return { directories };
|
|
1347
|
+
if ((0, import_node_path4.dirname)(directory) === directory)
|
|
1348
|
+
return { directories: [(0, import_node_path4.resolve)(cwd)] };
|
|
1349
|
+
directory = (0, import_node_path4.dirname)(directory);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
var read = async (file, privateFile) => {
|
|
1353
|
+
let stat;
|
|
1354
|
+
try {
|
|
1355
|
+
stat = await (0, import_promises3.lstat)(file);
|
|
1356
|
+
} catch {
|
|
1357
|
+
throw new Error(`Configured eval environment file is unavailable: ${file}`);
|
|
1358
|
+
}
|
|
1359
|
+
if (!stat.isFile() || privateFile && process.platform !== "win32" && ((stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`Eval configuration must be ${privateFile ? "an owner-only" : "a"} regular file: ${file}`
|
|
1362
|
+
);
|
|
1363
|
+
try {
|
|
1364
|
+
return await (0, import_promises3.readFile)(file, "utf8");
|
|
1365
|
+
} catch {
|
|
1366
|
+
throw new Error(`Cannot read eval configuration: ${file}`);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
async function loadEvalEnvironment(cwd, envFiles, shell = process.env) {
|
|
1370
|
+
let files;
|
|
1371
|
+
let privateFiles = false;
|
|
1372
|
+
if (envFiles?.length) {
|
|
1373
|
+
files = envFiles.map((file) => (0, import_node_path4.resolve)(cwd, file));
|
|
1374
|
+
} else {
|
|
1375
|
+
const { profile, directories } = await findConfiguration(cwd);
|
|
1376
|
+
if (profile) {
|
|
1377
|
+
let value;
|
|
1378
|
+
try {
|
|
1379
|
+
value = JSON.parse(await read(profile, true));
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (error instanceof SyntaxError)
|
|
1382
|
+
throw new Error(`Invalid eval environment profile: ${profile}`);
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
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(
|
|
1386
|
+
(file) => typeof file !== "string" || !file.trim()
|
|
1387
|
+
))
|
|
1388
|
+
throw new Error(`Invalid eval environment profile: ${profile}`);
|
|
1389
|
+
files = value.envFiles.map(
|
|
1390
|
+
(file) => (0, import_node_path4.resolve)((0, import_node_path4.dirname)(profile), file)
|
|
1391
|
+
);
|
|
1392
|
+
privateFiles = true;
|
|
1393
|
+
} else {
|
|
1394
|
+
files = [];
|
|
1395
|
+
for (const directory of directories)
|
|
1396
|
+
for (const name of [".env", ".env.local"]) {
|
|
1397
|
+
const file = (0, import_node_path4.resolve)(directory, name);
|
|
1398
|
+
if (await exists(file)) files.push(file);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const environment = {};
|
|
1403
|
+
for (const file of files)
|
|
1404
|
+
Object.assign(environment, (0, import_node_util.parseEnv)(await read(file, privateFiles)));
|
|
1405
|
+
return { ...environment, ...shell };
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/studio/command.ts
|
|
1409
|
+
var import_commander5 = require("commander");
|
|
1410
|
+
|
|
1411
|
+
// src/studio/eval-command.ts
|
|
1412
|
+
var import_commander3 = require("commander");
|
|
1413
|
+
|
|
1414
|
+
// src/studio/eval-client.ts
|
|
1415
|
+
var import_evals = require("@kortyx/agent/evals");
|
|
1416
|
+
var import_zod3 = require("zod");
|
|
1417
|
+
var StartRequestSchema = import_evals.StudioEvalStartRequestSchema;
|
|
1418
|
+
var parseEvalRunTarget = (input) => {
|
|
1419
|
+
let id = input;
|
|
1420
|
+
let url;
|
|
1421
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
|
|
1422
|
+
let locator;
|
|
1423
|
+
try {
|
|
1424
|
+
locator = new URL(input);
|
|
1425
|
+
} catch {
|
|
1426
|
+
throw new StudioReadError("invalid_target", "Invalid eval run URL.");
|
|
1427
|
+
}
|
|
1428
|
+
const match = locator.pathname.match(/\/evals\/runs\/([^/]+)\/?$/);
|
|
1429
|
+
if (!match || !["http:", "https:"].includes(locator.protocol) || locator.username || locator.password)
|
|
1430
|
+
throw new StudioReadError(
|
|
1431
|
+
"invalid_target",
|
|
1432
|
+
"Expected an HTTP(S) Studio /evals/runs/<uuid> URL without credentials."
|
|
1433
|
+
);
|
|
1434
|
+
id = match[1] ?? "";
|
|
1435
|
+
locator.search = "";
|
|
1436
|
+
locator.hash = "";
|
|
1437
|
+
url = locator.toString();
|
|
1438
|
+
}
|
|
1439
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1440
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1441
|
+
return { id, url };
|
|
1442
|
+
};
|
|
1443
|
+
var StudioEvalClient = class extends StudioApiTransport {
|
|
1444
|
+
targets() {
|
|
1445
|
+
return this.requestJson(
|
|
1446
|
+
"GET",
|
|
1447
|
+
"/v1/studio/evals/targets",
|
|
1448
|
+
import_evals.StudioEvalTargetsResponseSchema
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
runs() {
|
|
1452
|
+
return this.requestJson(
|
|
1453
|
+
"GET",
|
|
1454
|
+
"/v1/studio/evals/runs",
|
|
1455
|
+
import_evals.StudioEvalHistorySchema
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
run(id) {
|
|
1459
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1460
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1461
|
+
return this.requestJson(
|
|
1462
|
+
"GET",
|
|
1463
|
+
`/v1/studio/evals/runs/${id}`,
|
|
1464
|
+
import_evals.StudioEvalDetailSchema
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
start(input) {
|
|
1468
|
+
const parsed = StartRequestSchema.safeParse(input);
|
|
1469
|
+
if (!parsed.success)
|
|
1470
|
+
throw new StudioReadError(
|
|
1471
|
+
"invalid_eval_request",
|
|
1472
|
+
"Invalid suite revision, case selection, or execution limits."
|
|
1473
|
+
);
|
|
1474
|
+
return this.requestJson(
|
|
1475
|
+
"POST",
|
|
1476
|
+
"/v1/studio/evals/runs",
|
|
1477
|
+
import_zod3.z.object({ id: import_zod3.z.uuid() }),
|
|
1478
|
+
{},
|
|
1479
|
+
parsed.data
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
cancel(id) {
|
|
1483
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1484
|
+
throw new StudioReadError("invalid_target", "Expected an eval run UUID.");
|
|
1485
|
+
return this.requestJson(
|
|
1486
|
+
"POST",
|
|
1487
|
+
`/v1/studio/evals/runs/${id}/cancel`,
|
|
1488
|
+
import_zod3.z.object({ ok: import_zod3.z.literal(true) })
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/studio/read-output.ts
|
|
1494
|
+
var parseStudioTarget = (input, entity) => {
|
|
1495
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(input)) {
|
|
1496
|
+
let url;
|
|
1497
|
+
try {
|
|
1498
|
+
url = new URL(input);
|
|
1499
|
+
} catch {
|
|
1500
|
+
throw new StudioReadError("invalid_target", "Invalid Studio URL.");
|
|
1501
|
+
}
|
|
1502
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
1503
|
+
throw new StudioReadError(
|
|
1504
|
+
"invalid_target",
|
|
1505
|
+
"Use an HTTP(S) Studio entity URL without embedded credentials."
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const match = url.pathname.match(
|
|
1509
|
+
/\/(runs|sessions|interrupts)\/([^/]+)\/?$/
|
|
1510
|
+
);
|
|
1511
|
+
if (!match)
|
|
1512
|
+
throw new StudioReadError(
|
|
1513
|
+
"invalid_target",
|
|
1514
|
+
"Expected a Studio /runs/<id>, /sessions/<id>, or /interrupts/<id> URL."
|
|
1515
|
+
);
|
|
1516
|
+
if (entity && match[1] !== entity)
|
|
1517
|
+
throw new StudioReadError(
|
|
1518
|
+
"invalid_target",
|
|
1519
|
+
"URL entity type does not match the command."
|
|
1520
|
+
);
|
|
1521
|
+
let id;
|
|
1522
|
+
try {
|
|
1523
|
+
id = decodeURIComponent(match[2] ?? "");
|
|
1524
|
+
} catch {
|
|
1525
|
+
throw new StudioReadError(
|
|
1526
|
+
"invalid_target",
|
|
1527
|
+
"Invalid entity ID encoding."
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
validateId(id);
|
|
1531
|
+
const selection = {};
|
|
1532
|
+
for (const key of [
|
|
1533
|
+
"tab",
|
|
1534
|
+
"sessionTab",
|
|
1535
|
+
"call",
|
|
1536
|
+
"branch",
|
|
1537
|
+
"node",
|
|
1538
|
+
"event",
|
|
1539
|
+
"trace",
|
|
1540
|
+
"detailView"
|
|
1541
|
+
]) {
|
|
1542
|
+
const value = url.searchParams.get(key);
|
|
1543
|
+
if (value) selection[key] = value;
|
|
1544
|
+
}
|
|
1545
|
+
url.search = "";
|
|
1546
|
+
url.hash = "";
|
|
1547
|
+
return {
|
|
1548
|
+
entity: match[1],
|
|
1549
|
+
id,
|
|
1550
|
+
url: url.toString(),
|
|
1551
|
+
selection
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
if (!entity)
|
|
1555
|
+
throw new StudioReadError(
|
|
1556
|
+
"invalid_target",
|
|
1557
|
+
"inspect requires a Studio entity URL. Use runs/sessions/interrupts get for a bare ID."
|
|
1558
|
+
);
|
|
1559
|
+
validateId(input);
|
|
1560
|
+
return { entity, id: input };
|
|
1561
|
+
};
|
|
1562
|
+
var validateId = (id) => {
|
|
1563
|
+
if (!id || id.length > 512 || id === "." || id === ".." || /[\s/?#\\]/.test(id) || [...id].some(
|
|
1564
|
+
(char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
|
|
1565
|
+
)) {
|
|
1566
|
+
throw new StudioReadError("invalid_target", "Invalid Studio entity ID.");
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
var secretKey = /(?:password|secret|authorization|cookie|apikey|credential|resumetoken|accesstoken|refreshtoken|bearertoken|sessiontoken|authtoken)/i;
|
|
1570
|
+
var contentKey = /^(?:input|output|inputs|outputs|inputmessages|outputmessages|messages|prompt|systemprompt|question|response|result|latestresult|arguments|args|content|text|options)$/i;
|
|
1571
|
+
var sanitizeStudioData = (value, includeContent, key = "") => {
|
|
1572
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
1573
|
+
const keyPolicy = normalized.toLowerCase() === "apikey" && value && typeof value === "object" && "scopes" in value && "mode" in value;
|
|
1574
|
+
if (secretKey.test(normalized) && !keyPolicy) return "[REDACTED]";
|
|
1575
|
+
if (!includeContent && contentKey.test(normalized))
|
|
1576
|
+
return "[CONTENT OMITTED: use --include-content]";
|
|
1577
|
+
if (typeof value === "string")
|
|
1578
|
+
return value.replace(/ktyx_(?:test|live)_[^\s"'<>]+/g, "[REDACTED]").replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]");
|
|
1579
|
+
if (Array.isArray(value))
|
|
1580
|
+
return value.map((item) => sanitizeStudioData(item, includeContent));
|
|
1581
|
+
if (value && typeof value === "object")
|
|
1582
|
+
return Object.fromEntries(
|
|
1583
|
+
Object.entries(value).map(([name, item]) => [
|
|
1584
|
+
name,
|
|
1585
|
+
sanitizeStudioData(item, includeContent, name)
|
|
1586
|
+
])
|
|
1587
|
+
);
|
|
1588
|
+
return value;
|
|
1589
|
+
};
|
|
1590
|
+
var summarizeEvidence = (events) => {
|
|
1591
|
+
const findings = events.flatMap((event) => {
|
|
1592
|
+
const payload = event.payload;
|
|
1593
|
+
const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error) ? payload.error : {};
|
|
1594
|
+
const suspension = event.type === "span.failed" && (error.controlFlow === true || ["GraphInterrupt", "NodeInterrupt", "ParallelChildWaiting"].includes(
|
|
1595
|
+
String(error.name)
|
|
1596
|
+
));
|
|
1597
|
+
const failed = !suspension && (/(?:failed|error|fault)/i.test(event.type) || payload.outcome === "fault" || payload.status === "failed");
|
|
1598
|
+
const warning = suspension || /(?:retry|cancel|interrupt|limit_reached|denied|suspended|waiting)/i.test(
|
|
1599
|
+
event.type
|
|
1600
|
+
) || ["denied", "cancelled"].includes(String(payload.outcome));
|
|
1601
|
+
if (!failed && !warning) return [];
|
|
1602
|
+
return [
|
|
1603
|
+
{
|
|
1604
|
+
eventId: event.id,
|
|
1605
|
+
runId: event.runId,
|
|
1606
|
+
nodeId: event.nodeId,
|
|
1607
|
+
type: event.type,
|
|
1608
|
+
occurredAt: event.occurredAt,
|
|
1609
|
+
severity: failed ? "error" : "context",
|
|
1610
|
+
evidence: payload
|
|
1611
|
+
}
|
|
1612
|
+
];
|
|
1613
|
+
});
|
|
1614
|
+
return {
|
|
1615
|
+
findings,
|
|
1616
|
+
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."
|
|
1617
|
+
};
|
|
1618
|
+
};
|
|
1619
|
+
var formatReadOutput = (value, json) => {
|
|
1620
|
+
if (json) return JSON.stringify(value);
|
|
1621
|
+
return JSON.stringify(value, null, 2);
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
// src/studio/eval-command.ts
|
|
1625
|
+
var integer2 = (max) => (value) => {
|
|
1626
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
|
|
1627
|
+
throw new import_commander3.InvalidArgumentError(`Expected an integer between 1 and ${max}.`);
|
|
1628
|
+
return Number(value);
|
|
1629
|
+
};
|
|
1630
|
+
var summary = ({ error: _error, ...run2 }) => run2;
|
|
1631
|
+
var compactDetail = (run2) => ({
|
|
1632
|
+
...summary(run2),
|
|
1633
|
+
suite: void 0,
|
|
1634
|
+
events: void 0,
|
|
1635
|
+
result: void 0,
|
|
1636
|
+
caseResults: run2.result?.cases.map(
|
|
1637
|
+
({ caseId, repetition, status, durationMs, steps }) => ({
|
|
1638
|
+
caseId,
|
|
1639
|
+
repetition,
|
|
1640
|
+
status,
|
|
1641
|
+
durationMs,
|
|
1642
|
+
steps: steps.map(({ index, status: status2, criteria }) => ({
|
|
1643
|
+
index,
|
|
1644
|
+
status: status2,
|
|
1645
|
+
criteria: criteria.map(({ id, passed }) => ({ id, passed }))
|
|
1646
|
+
}))
|
|
1647
|
+
})
|
|
1648
|
+
)
|
|
1649
|
+
});
|
|
1650
|
+
function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
1651
|
+
const common = (command) => command.option(
|
|
1652
|
+
"--connection <name>",
|
|
1653
|
+
"Named project/API connection (or KORTYX_CONNECTION)."
|
|
1654
|
+
).option(
|
|
1655
|
+
"--config-home <path>",
|
|
1656
|
+
"Connection configuration directory.",
|
|
1657
|
+
defaultConnectionsHome()
|
|
1658
|
+
).option(
|
|
1659
|
+
"--home <path>",
|
|
1660
|
+
"Managed local Studio directory.",
|
|
1661
|
+
defaultStudioHome()
|
|
1662
|
+
).option("--api-url <url>", "Direct API URL; requires --api-key-env.").option(
|
|
1663
|
+
"--api-key-env <name>",
|
|
1664
|
+
"Environment variable holding a Studio key."
|
|
1665
|
+
).option("--json", "Print stable machine-readable JSON.").option(
|
|
1666
|
+
"--include-content",
|
|
1667
|
+
"Include suite definitions and captured execution content."
|
|
1668
|
+
);
|
|
1669
|
+
const print = (value, options) => log(
|
|
1670
|
+
formatReadOutput(
|
|
1671
|
+
sanitizeStudioData(
|
|
1672
|
+
{ schemaVersion: 1, ...value },
|
|
1673
|
+
options.includeContent ?? false
|
|
1674
|
+
),
|
|
1675
|
+
options.json ?? false
|
|
1676
|
+
)
|
|
1677
|
+
);
|
|
1678
|
+
const clientFor = async (options, url) => {
|
|
1679
|
+
const connection = await resolveConnection(options, url);
|
|
1680
|
+
return {
|
|
1681
|
+
connection,
|
|
1682
|
+
client: new StudioEvalClient(
|
|
1683
|
+
connection.apiUrl,
|
|
1684
|
+
connection.apiKey,
|
|
1685
|
+
request
|
|
1686
|
+
)
|
|
1687
|
+
};
|
|
1688
|
+
};
|
|
1689
|
+
const evals = studio.command("evals").description(
|
|
1690
|
+
"Discover suites, enqueue evals, inspect saved results and cancel runs."
|
|
1691
|
+
);
|
|
1692
|
+
const suites = evals.command("suites").description("Discover application-owned eval suites.");
|
|
1693
|
+
const discover = async (options) => {
|
|
1694
|
+
const { connection, client } = await clientFor(options);
|
|
1695
|
+
const data = await client.targets();
|
|
1696
|
+
const environment = options.environment ?? connection.environment;
|
|
1697
|
+
const targets = data.targets.filter(
|
|
1698
|
+
(target) => (!options.target || target.id === options.target) && (!environment || target.environment === environment)
|
|
1699
|
+
);
|
|
1700
|
+
return {
|
|
1701
|
+
connection,
|
|
1702
|
+
client,
|
|
1703
|
+
targets,
|
|
1704
|
+
canRun: data.canRun,
|
|
1705
|
+
studioJudge: data.studioJudge ?? null
|
|
1706
|
+
};
|
|
1707
|
+
};
|
|
1708
|
+
const selectionOptions = (command) => common(command).option("--target <id>", "Application eval target ID.").option(
|
|
1709
|
+
"--environment <name>",
|
|
1710
|
+
"Filter targets; defaults to the connection environment."
|
|
1711
|
+
);
|
|
1712
|
+
selectionOptions(
|
|
1713
|
+
suites.command("list").description("List targets, suites, revisions and case IDs.")
|
|
1714
|
+
).action(async (options) => {
|
|
1715
|
+
const { connection, targets, canRun, studioJudge } = await discover(options);
|
|
1716
|
+
print(
|
|
1717
|
+
{
|
|
1718
|
+
connection: connection.name,
|
|
1719
|
+
canRun,
|
|
1720
|
+
studioJudge,
|
|
1721
|
+
targets: targets.map((target) => ({
|
|
1722
|
+
id: target.id,
|
|
1723
|
+
name: target.name,
|
|
1724
|
+
environment: target.environment,
|
|
1725
|
+
available: target.manifest !== null,
|
|
1726
|
+
appJudge: target.manifest?.judge ?? null,
|
|
1727
|
+
studioJudging: Boolean(target.manifest?.studioJudging),
|
|
1728
|
+
suites: target.manifest?.suites.map((suite) => ({
|
|
1729
|
+
id: suite.id,
|
|
1730
|
+
name: suite.name,
|
|
1731
|
+
revision: target.revisions[suite.id],
|
|
1732
|
+
caseIds: suite.cases.map((item) => item.id)
|
|
1733
|
+
})) ?? []
|
|
1734
|
+
}))
|
|
1735
|
+
},
|
|
1736
|
+
options
|
|
1737
|
+
);
|
|
1738
|
+
});
|
|
1739
|
+
const select = async (suiteId, options) => {
|
|
1740
|
+
const data = await discover(options);
|
|
1741
|
+
const matches = data.targets.flatMap((target) => {
|
|
1742
|
+
const suite = target.manifest?.suites.find((item) => item.id === suiteId);
|
|
1743
|
+
return suite ? [{ target, suite }] : [];
|
|
1744
|
+
});
|
|
1745
|
+
const match = matches[0];
|
|
1746
|
+
if (matches.length !== 1 || !match)
|
|
848
1747
|
throw new StudioReadError(
|
|
849
|
-
"
|
|
850
|
-
"
|
|
1748
|
+
"invalid_suite_selection",
|
|
1749
|
+
matches.length ? "Suite exists on multiple targets. Select --target and/or --environment." : "Suite unavailable. List suites and check the target connection."
|
|
851
1750
|
);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1751
|
+
return { ...data, ...match };
|
|
1752
|
+
};
|
|
1753
|
+
selectionOptions(
|
|
1754
|
+
suites.command("get <suite-id>").description(
|
|
1755
|
+
"Read a suite; --include-content returns its full definition."
|
|
1756
|
+
)
|
|
1757
|
+
).action(async (suiteId, options) => {
|
|
1758
|
+
const { connection, target, suite } = await select(suiteId, options);
|
|
1759
|
+
print(
|
|
1760
|
+
{
|
|
1761
|
+
connection: connection.name,
|
|
1762
|
+
targetId: target.id,
|
|
1763
|
+
revision: target.revisions[suite.id],
|
|
1764
|
+
suite: options.includeContent ? suite : {
|
|
1765
|
+
id: suite.id,
|
|
1766
|
+
name: suite.name,
|
|
1767
|
+
caseIds: suite.cases.map((item) => item.id)
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
options
|
|
1771
|
+
);
|
|
856
1772
|
});
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1773
|
+
const runs = evals.command("runs").description("Start and inspect saved eval runs.");
|
|
1774
|
+
selectionOptions(
|
|
1775
|
+
runs.command("start <suite-id>").description(
|
|
1776
|
+
"Enqueue once and return immediately; does not wait for grades."
|
|
860
1777
|
)
|
|
861
|
-
).
|
|
862
|
-
|
|
863
|
-
|
|
1778
|
+
).option(
|
|
1779
|
+
"--case <id>",
|
|
1780
|
+
"Select a case; repeat for multiple cases.",
|
|
1781
|
+
(value, previous) => [...previous, value],
|
|
1782
|
+
[]
|
|
1783
|
+
).option(
|
|
1784
|
+
"--repetitions <count>",
|
|
1785
|
+
"Attempts per selected case (1\u201320).",
|
|
1786
|
+
integer2(20),
|
|
1787
|
+
1
|
|
1788
|
+
).option("--concurrency <count>", "Parallel attempts (1\u20134).", integer2(4), 1).option(
|
|
1789
|
+
"--judge <location>",
|
|
1790
|
+
"Judge selection: studio (default) or app.",
|
|
1791
|
+
(value) => {
|
|
1792
|
+
if (value !== "studio" && value !== "app")
|
|
1793
|
+
throw new import_commander3.InvalidArgumentError("Expected studio or app.");
|
|
1794
|
+
return value;
|
|
1795
|
+
},
|
|
1796
|
+
"studio"
|
|
1797
|
+
).action(async (suiteId, options) => {
|
|
1798
|
+
const { connection, client, target, suite, canRun } = await select(
|
|
1799
|
+
suiteId,
|
|
1800
|
+
options
|
|
1801
|
+
);
|
|
1802
|
+
if (!canRun)
|
|
864
1803
|
throw new StudioReadError(
|
|
865
|
-
"
|
|
866
|
-
"
|
|
1804
|
+
"eval_forbidden",
|
|
1805
|
+
"This Studio key requires eval:run to execute suites."
|
|
867
1806
|
);
|
|
868
|
-
|
|
869
|
-
|
|
1807
|
+
const caseIds = options.case ?? [];
|
|
1808
|
+
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)
|
|
1809
|
+
throw new StudioReadError(
|
|
1810
|
+
"invalid_case_selection",
|
|
1811
|
+
"Select unique existing case IDs and at most 100 total attempts."
|
|
1812
|
+
);
|
|
1813
|
+
const run2 = await client.start({
|
|
1814
|
+
targetId: target.id,
|
|
1815
|
+
judge: options.judge ?? "studio",
|
|
1816
|
+
suiteId,
|
|
1817
|
+
suiteRevision: target.revisions[suiteId] ?? "",
|
|
1818
|
+
repetitions: options.repetitions,
|
|
1819
|
+
concurrency: options.concurrency,
|
|
1820
|
+
...caseIds.length ? { caseIds } : {}
|
|
1821
|
+
});
|
|
1822
|
+
print(
|
|
1823
|
+
{
|
|
1824
|
+
connection: connection.name,
|
|
1825
|
+
id: run2.id,
|
|
1826
|
+
status: "queued",
|
|
1827
|
+
studioUrl: connection.studioUrl ? `${connection.studioUrl}/evals/runs/${run2.id}` : null
|
|
1828
|
+
},
|
|
1829
|
+
options
|
|
870
1830
|
);
|
|
871
|
-
if (config.current === name) delete config.current;
|
|
872
|
-
await saveConnections(config, options.configHome);
|
|
873
|
-
print({ removed: name }, options);
|
|
874
1831
|
});
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1832
|
+
common(
|
|
1833
|
+
runs.command("list").description("List the latest 100 saved runs in this project.")
|
|
1834
|
+
).option(
|
|
1835
|
+
"--environment <name>",
|
|
1836
|
+
"Filter results; defaults to the connection environment."
|
|
1837
|
+
).action(async (options) => {
|
|
1838
|
+
const { connection, client } = await clientFor(options);
|
|
1839
|
+
const data = await client.runs();
|
|
1840
|
+
const environment = options.environment ?? connection.environment;
|
|
1841
|
+
print(
|
|
1842
|
+
{
|
|
1843
|
+
connection: connection.name,
|
|
1844
|
+
runs: data.runs.filter((run2) => !environment || run2.environment === environment).map(summary)
|
|
1845
|
+
},
|
|
1846
|
+
options
|
|
1847
|
+
);
|
|
1848
|
+
});
|
|
1849
|
+
common(
|
|
1850
|
+
runs.command("get <id-or-url>").description(
|
|
1851
|
+
"Read status, counts and criterion verdicts; content is opt-in."
|
|
1852
|
+
)
|
|
1853
|
+
).action(async (input, options) => {
|
|
1854
|
+
const target = parseEvalRunTarget(input);
|
|
1855
|
+
const { connection, client } = await clientFor(options, target.url);
|
|
1856
|
+
const { run: run2 } = await client.run(target.id);
|
|
1857
|
+
print(
|
|
1858
|
+
{
|
|
1859
|
+
connection: connection.name,
|
|
1860
|
+
run: options.includeContent ? run2 : compactDetail(run2)
|
|
1861
|
+
},
|
|
1862
|
+
options
|
|
1863
|
+
);
|
|
1864
|
+
});
|
|
1865
|
+
common(
|
|
1866
|
+
runs.command("cancel <id-or-url>").description(
|
|
1867
|
+
"Request cooperative cancellation of a queued or running eval."
|
|
1868
|
+
)
|
|
1869
|
+
).action(async (input, options) => {
|
|
1870
|
+
const target = parseEvalRunTarget(input);
|
|
1871
|
+
const { connection, client } = await clientFor(options, target.url);
|
|
1872
|
+
const data = await client.cancel(target.id);
|
|
1873
|
+
print({ connection: connection.name, id: target.id, ...data }, options);
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
880
1876
|
|
|
881
1877
|
// src/studio/local-stack.ts
|
|
882
|
-
var
|
|
1878
|
+
var import_promises4 = require("fs/promises");
|
|
883
1879
|
var import_telemetry_contracts3 = require("@kortyx/telemetry-contracts");
|
|
884
1880
|
var composeArgs = (home, args) => [
|
|
885
1881
|
"compose",
|
|
@@ -1033,9 +2029,20 @@ var startStudio = async (options, runtime) => {
|
|
|
1033
2029
|
const studioImage = String(
|
|
1034
2030
|
environment.KORTYX_STUDIO_IMAGE_REF ?? `${environment.KORTYX_STUDIO_IMAGE ?? "ghcr.io/kortyx-io/kortyx-studio"}:${config.imageTag}`
|
|
1035
2031
|
);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
2032
|
+
const pullPolicy = process.env.KORTYX_STUDIO_PULL_POLICY ?? "always";
|
|
2033
|
+
for (const image of /* @__PURE__ */ new Set([apiImage, studioImage])) {
|
|
2034
|
+
if (pullPolicy === "never") continue;
|
|
2035
|
+
if (pullPolicy === "missing") {
|
|
2036
|
+
try {
|
|
2037
|
+
await runtime.run("docker", ["image", "inspect", image], {
|
|
2038
|
+
inherit: false
|
|
2039
|
+
});
|
|
2040
|
+
continue;
|
|
2041
|
+
} catch {
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
await runtime.run("docker", ["pull", image], { inherit: true });
|
|
2045
|
+
}
|
|
1039
2046
|
const capability = await runtime.run(
|
|
1040
2047
|
"docker",
|
|
1041
2048
|
[
|
|
@@ -1049,7 +2056,7 @@ var startStudio = async (options, runtime) => {
|
|
|
1049
2056
|
],
|
|
1050
2057
|
{ inherit: false }
|
|
1051
2058
|
);
|
|
1052
|
-
await (0,
|
|
2059
|
+
await (0, import_promises4.writeFile)(
|
|
1053
2060
|
studioComposePath(options.home),
|
|
1054
2061
|
studioComposeFile(
|
|
1055
2062
|
process.platform !== "win32" && capability.stdout.trim() === "supported"
|
|
@@ -1152,7 +2159,7 @@ var resetStudio = async (home, confirm, runtime) => {
|
|
|
1152
2159
|
|
|
1153
2160
|
// src/studio/read-command.ts
|
|
1154
2161
|
var import_telemetry_contracts5 = require("@kortyx/telemetry-contracts");
|
|
1155
|
-
var
|
|
2162
|
+
var import_commander4 = require("commander");
|
|
1156
2163
|
|
|
1157
2164
|
// src/studio/read-analysis.ts
|
|
1158
2165
|
var import_telemetry_contracts4 = require("@kortyx/telemetry-contracts");
|
|
@@ -1459,141 +2466,10 @@ var compareRunAnalysis = (left, right) => {
|
|
|
1459
2466
|
};
|
|
1460
2467
|
};
|
|
1461
2468
|
|
|
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
2469
|
// src/studio/read-command.ts
|
|
1594
|
-
var
|
|
2470
|
+
var integer3 = (min, max) => (value) => {
|
|
1595
2471
|
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
|
|
1596
|
-
throw new
|
|
2472
|
+
throw new import_commander4.InvalidArgumentError(
|
|
1597
2473
|
`Expected an integer between ${min} and ${max}.`
|
|
1598
2474
|
);
|
|
1599
2475
|
}
|
|
@@ -1749,7 +2625,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1749
2625
|
const detailOptions = (command) => common(command).option(
|
|
1750
2626
|
"--event-limit <count>",
|
|
1751
2627
|
"Maximum latest events and diagnostic findings (1\u201310000).",
|
|
1752
|
-
|
|
2628
|
+
integer3(1, 1e4),
|
|
1753
2629
|
100
|
|
1754
2630
|
).option(
|
|
1755
2631
|
"--focus-selection",
|
|
@@ -1802,7 +2678,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1802
2678
|
"--environment <name>",
|
|
1803
2679
|
"Environment filter; overrides the connection default."
|
|
1804
2680
|
).addOption(
|
|
1805
|
-
new
|
|
2681
|
+
new import_commander4.Option(
|
|
1806
2682
|
"--range <range>",
|
|
1807
2683
|
"Studio time range: 1h, 24h, 7d, 30d, all (or Studio preset name)."
|
|
1808
2684
|
).choices([
|
|
@@ -1847,7 +2723,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1847
2723
|
).option(
|
|
1848
2724
|
"--event-limit <count>",
|
|
1849
2725
|
"Maximum timeline steps returned per run (1\u201310000).",
|
|
1850
|
-
|
|
2726
|
+
integer3(1, 1e4),
|
|
1851
2727
|
100
|
|
1852
2728
|
).action(
|
|
1853
2729
|
async (leftInput, rightInput, options) => {
|
|
@@ -1947,13 +2823,13 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
1947
2823
|
}
|
|
1948
2824
|
const list = filters(
|
|
1949
2825
|
group.command("list").description("Read a bounded, filtered page.")
|
|
1950
|
-
).option("--limit <count>", "Page size (1\u2013100).",
|
|
2826
|
+
).option("--limit <count>", "Page size (1\u2013100).", integer3(1, 100), 25).option(
|
|
1951
2827
|
"--cursor <offset>",
|
|
1952
2828
|
"Numeric offset returned as nextCursor.",
|
|
1953
|
-
|
|
2829
|
+
integer3(0, Number.MAX_SAFE_INTEGER),
|
|
1954
2830
|
0
|
|
1955
2831
|
).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
|
|
2832
|
+
new import_commander4.Option("--tool-outcome <outcome>", "Tool outcome filter.").choices([
|
|
1957
2833
|
"success",
|
|
1958
2834
|
"denied",
|
|
1959
2835
|
"fault",
|
|
@@ -2082,14 +2958,14 @@ var defaultStudioRuntime = {
|
|
|
2082
2958
|
var portParser = (value) => {
|
|
2083
2959
|
const port = Number(value);
|
|
2084
2960
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2085
|
-
throw new
|
|
2961
|
+
throw new import_commander5.InvalidArgumentError(
|
|
2086
2962
|
"Port must be an integer between 1 and 65535."
|
|
2087
2963
|
);
|
|
2088
2964
|
}
|
|
2089
2965
|
return port;
|
|
2090
2966
|
};
|
|
2091
2967
|
var withHome = (command) => command.addOption(
|
|
2092
|
-
new
|
|
2968
|
+
new import_commander5.Option("--home <path>", "Local Studio state directory.").default(
|
|
2093
2969
|
defaultStudioHome()
|
|
2094
2970
|
)
|
|
2095
2971
|
);
|
|
@@ -2099,7 +2975,7 @@ var configureOutput = (command, runtime) => command.configureOutput({
|
|
|
2099
2975
|
});
|
|
2100
2976
|
var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
2101
2977
|
const studio = configureOutput(
|
|
2102
|
-
new
|
|
2978
|
+
new import_commander5.Command("studio").description("Run and manage Kortyx Studio.").showHelpAfterError().addHelpText(
|
|
2103
2979
|
"after",
|
|
2104
2980
|
"\nLocal state and generated credentials are stored in ~/.kortyx/studio by default."
|
|
2105
2981
|
),
|
|
@@ -2142,7 +3018,7 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
|
2142
3018
|
"Generate unpersisted credentials for a self-hosted deployment.",
|
|
2143
3019
|
false
|
|
2144
3020
|
).addOption(
|
|
2145
|
-
new
|
|
3021
|
+
new import_commander5.Option("--format <format>", "Credential output format.").choices(["human", "dotenv"]).default("human")
|
|
2146
3022
|
).option(
|
|
2147
3023
|
"--service-name <name>",
|
|
2148
3024
|
"SDK service name used by dotenv output.",
|
|
@@ -2184,20 +3060,29 @@ var createStudioCommand = (runtime = defaultStudioRuntime) => {
|
|
|
2184
3060
|
).action(async ({ home, confirm }) => {
|
|
2185
3061
|
await resetStudio(home, confirm, runtime);
|
|
2186
3062
|
});
|
|
2187
|
-
registerStudioReadCommands(
|
|
3063
|
+
registerStudioReadCommands(
|
|
3064
|
+
studio,
|
|
3065
|
+
(message) => runtime.log(message),
|
|
3066
|
+
runtime.request
|
|
3067
|
+
);
|
|
3068
|
+
registerStudioEvalCommands(
|
|
3069
|
+
studio,
|
|
3070
|
+
(message) => runtime.log(message),
|
|
3071
|
+
runtime.request
|
|
3072
|
+
);
|
|
2188
3073
|
return studio;
|
|
2189
3074
|
};
|
|
2190
3075
|
|
|
2191
3076
|
// src/workflow-calls.ts
|
|
2192
|
-
var
|
|
3077
|
+
var import_node_path5 = require("path");
|
|
2193
3078
|
var import_typescript = __toESM(require("typescript"));
|
|
2194
3079
|
function discoverWorkflowCalls(entry, snapshots) {
|
|
2195
|
-
const configPath = import_typescript.default.findConfigFile((0,
|
|
3080
|
+
const configPath = import_typescript.default.findConfigFile((0, import_node_path5.dirname)(entry), import_typescript.default.sys.fileExists);
|
|
2196
3081
|
const config = configPath ? import_typescript.default.readConfigFile(configPath, import_typescript.default.sys.readFile).config : {};
|
|
2197
3082
|
const parsed = import_typescript.default.parseJsonConfigFileContent(
|
|
2198
3083
|
config ?? {},
|
|
2199
3084
|
import_typescript.default.sys,
|
|
2200
|
-
configPath ? (0,
|
|
3085
|
+
configPath ? (0, import_node_path5.dirname)(configPath) : (0, import_node_path5.dirname)(entry)
|
|
2201
3086
|
);
|
|
2202
3087
|
const program = import_typescript.default.createProgram([entry], {
|
|
2203
3088
|
...parsed.options,
|
|
@@ -2238,7 +3123,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2238
3123
|
);
|
|
2239
3124
|
};
|
|
2240
3125
|
const factoryBindings = /* @__PURE__ */ new WeakMap();
|
|
2241
|
-
function
|
|
3126
|
+
function resolve6(e, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
2242
3127
|
if (!e) return;
|
|
2243
3128
|
e = unwrap(e);
|
|
2244
3129
|
if (seen.has(e)) return;
|
|
@@ -2246,18 +3131,18 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2246
3131
|
if (import_typescript.default.isIdentifier(e)) {
|
|
2247
3132
|
const s = symbol(e);
|
|
2248
3133
|
const bound = s && bindings.get(s);
|
|
2249
|
-
if (bound) return
|
|
3134
|
+
if (bound) return resolve6(bound, bindings, seen);
|
|
2250
3135
|
const d = s?.valueDeclaration;
|
|
2251
3136
|
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
|
|
3137
|
+
return resolve6(d.initializer, bindings, seen);
|
|
2253
3138
|
}
|
|
2254
3139
|
if (import_typescript.default.isPropertyAccessExpression(e)) {
|
|
2255
3140
|
const found = property(e.expression, e.name.text, bindings, seen);
|
|
2256
|
-
if (found) return
|
|
3141
|
+
if (found) return resolve6(found, bindings, seen);
|
|
2257
3142
|
const owner = symbol(e.expression);
|
|
2258
3143
|
const declaration = symbol(e.name)?.valueDeclaration;
|
|
2259
3144
|
if (owner && owner.flags & import_typescript.default.SymbolFlags.Module && declaration && import_typescript.default.isVariableDeclaration(declaration))
|
|
2260
|
-
return
|
|
3145
|
+
return resolve6(declaration.initializer, bindings, seen);
|
|
2261
3146
|
return e;
|
|
2262
3147
|
}
|
|
2263
3148
|
if (import_typescript.default.isElementAccessExpression(e) && e.argumentExpression && import_typescript.default.isStringLiteralLike(e.argumentExpression)) {
|
|
@@ -2267,7 +3152,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2267
3152
|
bindings,
|
|
2268
3153
|
seen
|
|
2269
3154
|
);
|
|
2270
|
-
return found ?
|
|
3155
|
+
return found ? resolve6(found, bindings, seen) : e;
|
|
2271
3156
|
}
|
|
2272
3157
|
if (import_typescript.default.isCallExpression(e)) {
|
|
2273
3158
|
const d = symbol(
|
|
@@ -2281,12 +3166,12 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2281
3166
|
const key = symbol(p.name);
|
|
2282
3167
|
const arg = e.arguments[i] ?? p.initializer;
|
|
2283
3168
|
if (key && arg)
|
|
2284
|
-
bound.set(key,
|
|
3169
|
+
bound.set(key, resolve6(arg, bindings, new Set(seen)) ?? arg);
|
|
2285
3170
|
}
|
|
2286
3171
|
});
|
|
2287
3172
|
const returned = import_typescript.default.isBlock(fn.body) ? fn.body.statements.filter(import_typescript.default.isReturnStatement).map((r) => r.expression) : [fn.body];
|
|
2288
3173
|
if (returned.length === 1 && returned[0]) {
|
|
2289
|
-
const value =
|
|
3174
|
+
const value = resolve6(returned[0], bound, seen);
|
|
2290
3175
|
if (value) factoryBindings.set(value, bound);
|
|
2291
3176
|
return value;
|
|
2292
3177
|
}
|
|
@@ -2295,30 +3180,30 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2295
3180
|
return e;
|
|
2296
3181
|
}
|
|
2297
3182
|
function property(e, key, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
2298
|
-
const obj =
|
|
3183
|
+
const obj = resolve6(e, bindings, seen);
|
|
2299
3184
|
if (!obj || !import_typescript.default.isObjectLiteralExpression(obj)) return;
|
|
2300
3185
|
const scoped = factoryBindings.get(obj) ?? bindings;
|
|
2301
3186
|
for (const p of [...obj.properties].reverse()) {
|
|
2302
3187
|
if (import_typescript.default.isSpreadAssignment(p)) {
|
|
2303
|
-
const spread =
|
|
3188
|
+
const spread = resolve6(p.expression, scoped, new Set(seen));
|
|
2304
3189
|
if (!spread || !import_typescript.default.isObjectLiteralExpression(spread)) return void 0;
|
|
2305
3190
|
const found = property(p.expression, key, scoped, new Set(seen));
|
|
2306
3191
|
if (found) return found;
|
|
2307
3192
|
} else if (p.name && (import_typescript.default.isIdentifier(p.name) || import_typescript.default.isStringLiteralLike(p.name)) && p.name.text === key) {
|
|
2308
3193
|
if (import_typescript.default.isPropertyAssignment(p))
|
|
2309
|
-
return
|
|
3194
|
+
return resolve6(p.initializer, scoped, new Set(seen));
|
|
2310
3195
|
if (import_typescript.default.isShorthandPropertyAssignment(p))
|
|
2311
|
-
return
|
|
3196
|
+
return resolve6(p.name, scoped, new Set(seen));
|
|
2312
3197
|
}
|
|
2313
3198
|
}
|
|
2314
3199
|
return void 0;
|
|
2315
3200
|
}
|
|
2316
3201
|
function literal(e, bindings) {
|
|
2317
|
-
const value =
|
|
3202
|
+
const value = resolve6(e, bindings);
|
|
2318
3203
|
return value && import_typescript.default.isStringLiteralLike(value) ? value.text : void 0;
|
|
2319
3204
|
}
|
|
2320
3205
|
function workflowId(e, bindings) {
|
|
2321
|
-
const value =
|
|
3206
|
+
const value = resolve6(e, bindings);
|
|
2322
3207
|
if (!value) return;
|
|
2323
3208
|
if (import_typescript.default.isStringLiteralLike(value)) return value.text;
|
|
2324
3209
|
if (import_typescript.default.isCallExpression(value) && api(value.expression, "defineWorkflow"))
|
|
@@ -2334,19 +3219,19 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2334
3219
|
if (d && import_typescript.default.isBindingElement(d) && (d.propertyName?.getText() ?? d.name.getText()) === "useWorkflow") {
|
|
2335
3220
|
const decl = d.parent.parent;
|
|
2336
3221
|
if (import_typescript.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
2337
|
-
const value2 =
|
|
3222
|
+
const value2 = resolve6(decl.initializer, /* @__PURE__ */ new Map());
|
|
2338
3223
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
2339
3224
|
}
|
|
2340
3225
|
}
|
|
2341
3226
|
if (import_typescript.default.isPropertyAccessExpression(e) && e.name.text === "useWorkflow") {
|
|
2342
|
-
const value2 =
|
|
3227
|
+
const value2 = resolve6(e.expression, /* @__PURE__ */ new Map());
|
|
2343
3228
|
return !!value2 && import_typescript.default.isCallExpression(value2) && api(value2.expression, "createWorkflowHooks");
|
|
2344
3229
|
}
|
|
2345
|
-
const value =
|
|
3230
|
+
const value = resolve6(e, /* @__PURE__ */ new Map());
|
|
2346
3231
|
return !!value && value !== e && api(value, "useWorkflow");
|
|
2347
3232
|
}
|
|
2348
3233
|
function functionFor(e) {
|
|
2349
|
-
const value =
|
|
3234
|
+
const value = resolve6(e, /* @__PURE__ */ new Map());
|
|
2350
3235
|
if (value && (import_typescript.default.isArrowFunction(value) || import_typescript.default.isFunctionExpression(value)))
|
|
2351
3236
|
return value;
|
|
2352
3237
|
const d = symbol(
|
|
@@ -2369,7 +3254,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2369
3254
|
target.toolDiscovery.warnings.push(warning);
|
|
2370
3255
|
warnings.add(warning);
|
|
2371
3256
|
};
|
|
2372
|
-
const value =
|
|
3257
|
+
const value = resolve6(expression, bindings);
|
|
2373
3258
|
if (!value || seen.has(value)) {
|
|
2374
3259
|
unresolved();
|
|
2375
3260
|
return;
|
|
@@ -2414,8 +3299,8 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2414
3299
|
bindings
|
|
2415
3300
|
);
|
|
2416
3301
|
const schema = property(value, "inputSchema", bindings);
|
|
2417
|
-
const fields =
|
|
2418
|
-
const requiredValue =
|
|
3302
|
+
const fields = resolve6(property(schema, "properties", bindings), bindings);
|
|
3303
|
+
const requiredValue = resolve6(
|
|
2419
3304
|
property(schema, "required", bindings),
|
|
2420
3305
|
bindings
|
|
2421
3306
|
);
|
|
@@ -2504,7 +3389,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2504
3389
|
if (!arg) return;
|
|
2505
3390
|
if (import_typescript.default.isIdentifier(p.name)) {
|
|
2506
3391
|
const s = symbol(p.name);
|
|
2507
|
-
if (s) bound.set(s,
|
|
3392
|
+
if (s) bound.set(s, resolve6(arg, bindings) ?? arg);
|
|
2508
3393
|
} else if (import_typescript.default.isObjectBindingPattern(p.name)) {
|
|
2509
3394
|
for (const b of p.name.elements) {
|
|
2510
3395
|
const value = property(
|
|
@@ -2514,7 +3399,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2514
3399
|
);
|
|
2515
3400
|
const s = symbol(b.name);
|
|
2516
3401
|
if (s && value)
|
|
2517
|
-
bound.set(s,
|
|
3402
|
+
bound.set(s, resolve6(value, bindings) ?? value);
|
|
2518
3403
|
}
|
|
2519
3404
|
}
|
|
2520
3405
|
});
|
|
@@ -2529,7 +3414,7 @@ function discoverWorkflowCalls(entry, snapshots) {
|
|
|
2529
3414
|
for (const source of program.getSourceFiles().filter(local)) {
|
|
2530
3415
|
const walk = (n) => {
|
|
2531
3416
|
if (import_typescript.default.isCallExpression(n) && api(n.expression, "defineWorkflow")) {
|
|
2532
|
-
const obj =
|
|
3417
|
+
const obj = resolve6(n.arguments[0], /* @__PURE__ */ new Map());
|
|
2533
3418
|
const id = obj && literal(property(obj, "id", /* @__PURE__ */ new Map()), /* @__PURE__ */ new Map());
|
|
2534
3419
|
if (id && known.has(id) && obj && import_typescript.default.isObjectLiteralExpression(obj))
|
|
2535
3420
|
definitions.set(id, [...definitions.get(id) ?? [], obj]);
|
|
@@ -2599,24 +3484,24 @@ var readPackageName = async (cwd) => {
|
|
|
2599
3484
|
let current = cwd;
|
|
2600
3485
|
for (; ; ) {
|
|
2601
3486
|
try {
|
|
2602
|
-
const raw = await (0,
|
|
3487
|
+
const raw = await (0, import_promises5.readFile)((0, import_node_path6.join)(current, "package.json"), "utf8");
|
|
2603
3488
|
const parsed = JSON.parse(raw);
|
|
2604
3489
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
|
2605
3490
|
return parsed.name;
|
|
2606
3491
|
}
|
|
2607
3492
|
} catch {
|
|
2608
3493
|
}
|
|
2609
|
-
const parent = (0,
|
|
3494
|
+
const parent = (0, import_node_path6.dirname)(current);
|
|
2610
3495
|
if (parent === current) return void 0;
|
|
2611
3496
|
current = parent;
|
|
2612
3497
|
}
|
|
2613
3498
|
};
|
|
2614
3499
|
var loadEnvFiles = async (cwd) => {
|
|
2615
|
-
for (const filename of [".env", ".env
|
|
2616
|
-
const path = (0,
|
|
3500
|
+
for (const filename of [".env.local", ".env"]) {
|
|
3501
|
+
const path = (0, import_node_path6.join)(cwd, filename);
|
|
2617
3502
|
let raw;
|
|
2618
3503
|
try {
|
|
2619
|
-
raw = await (0,
|
|
3504
|
+
raw = await (0, import_promises5.readFile)(path, "utf8");
|
|
2620
3505
|
} catch {
|
|
2621
3506
|
continue;
|
|
2622
3507
|
}
|
|
@@ -2642,7 +3527,7 @@ var resolveOptions = async (options) => {
|
|
|
2642
3527
|
if (!options.entry) {
|
|
2643
3528
|
throw new Error("Missing required option: --entry <path>.");
|
|
2644
3529
|
}
|
|
2645
|
-
const entry = (0,
|
|
3530
|
+
const entry = (0, import_node_path6.isAbsolute)(options.entry) ? options.entry : (0, import_node_path6.resolve)(options.cwd, options.entry);
|
|
2646
3531
|
const environment = options.environment ?? process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
|
|
2647
3532
|
const serviceName = options.serviceName ?? process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? await readPackageName(options.cwd) ?? "kortyx-app";
|
|
2648
3533
|
return {
|
|
@@ -2656,19 +3541,19 @@ var resolveOptions = async (options) => {
|
|
|
2656
3541
|
};
|
|
2657
3542
|
};
|
|
2658
3543
|
var createFrameworkMarkerStubs = async () => {
|
|
2659
|
-
const root = await (0,
|
|
3544
|
+
const root = await (0, import_promises5.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
|
|
2660
3545
|
for (const moduleName of ["server-only", "client-only"]) {
|
|
2661
|
-
const directory = (0,
|
|
2662
|
-
await (0,
|
|
2663
|
-
await (0,
|
|
2664
|
-
await (0,
|
|
2665
|
-
(0,
|
|
3546
|
+
const directory = (0, import_node_path6.join)(root, moduleName);
|
|
3547
|
+
await (0, import_promises5.mkdir)(directory, { recursive: true });
|
|
3548
|
+
await (0, import_promises5.writeFile)((0, import_node_path6.join)(directory, "index.js"), "", "utf8");
|
|
3549
|
+
await (0, import_promises5.writeFile)(
|
|
3550
|
+
(0, import_node_path6.join)(directory, "package.json"),
|
|
2666
3551
|
JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
|
|
2667
3552
|
"utf8"
|
|
2668
3553
|
);
|
|
2669
3554
|
}
|
|
2670
3555
|
const previousNodePath = process.env.NODE_PATH;
|
|
2671
|
-
process.env.NODE_PATH = previousNodePath ? `${root}${
|
|
3556
|
+
process.env.NODE_PATH = previousNodePath ? `${root}${import_node_path6.delimiter}${previousNodePath}` : root;
|
|
2672
3557
|
const moduleWithPrivateInit = import_node_module.default;
|
|
2673
3558
|
moduleWithPrivateInit._initPaths?.();
|
|
2674
3559
|
return {
|
|
@@ -2676,48 +3561,71 @@ var createFrameworkMarkerStubs = async () => {
|
|
|
2676
3561
|
if (previousNodePath === void 0) delete process.env.NODE_PATH;
|
|
2677
3562
|
else process.env.NODE_PATH = previousNodePath;
|
|
2678
3563
|
moduleWithPrivateInit._initPaths?.();
|
|
2679
|
-
await (0,
|
|
3564
|
+
await (0, import_promises5.rm)(root, { recursive: true, force: true });
|
|
2680
3565
|
}
|
|
2681
3566
|
};
|
|
2682
3567
|
};
|
|
2683
|
-
var
|
|
3568
|
+
var openEntry = async (entry) => {
|
|
2684
3569
|
const stubs = await createFrameworkMarkerStubs();
|
|
2685
3570
|
let unregisterPaths;
|
|
3571
|
+
let unregisterEsm;
|
|
3572
|
+
const unregisterTsx = (0, import_api.register)();
|
|
3573
|
+
const release = async () => {
|
|
3574
|
+
unregisterPaths?.();
|
|
3575
|
+
unregisterTsx();
|
|
3576
|
+
await unregisterEsm?.();
|
|
3577
|
+
await stubs.cleanup();
|
|
3578
|
+
};
|
|
2686
3579
|
try {
|
|
2687
|
-
const tsconfig = await findNearestFile((0,
|
|
3580
|
+
const tsconfig = await findNearestFile((0, import_node_path6.dirname)(entry), "tsconfig.json");
|
|
3581
|
+
unregisterEsm = (0, import_api2.register)(tsconfig ? { tsconfig } : {});
|
|
2688
3582
|
unregisterPaths = tsconfig ? await registerTsconfigPaths(tsconfig) : void 0;
|
|
2689
|
-
const module2 = (0,
|
|
3583
|
+
const module2 = (0, import_node_module.createRequire)((0, import_node_path6.join)((0, import_node_path6.dirname)(entry), "kortyx-cli.cjs"))(entry);
|
|
2690
3584
|
if (!isRecord(module2)) {
|
|
2691
3585
|
throw new Error(`Entry module did not export an object: ${entry}`);
|
|
2692
3586
|
}
|
|
2693
|
-
return module2;
|
|
3587
|
+
return { module: module2, release };
|
|
3588
|
+
} catch (error) {
|
|
3589
|
+
await release();
|
|
3590
|
+
throw error;
|
|
3591
|
+
}
|
|
3592
|
+
};
|
|
3593
|
+
var importEntry = async (entry) => {
|
|
3594
|
+
const loaded = await openEntry(entry);
|
|
3595
|
+
try {
|
|
3596
|
+
return loaded.module;
|
|
2694
3597
|
} finally {
|
|
2695
|
-
|
|
2696
|
-
await stubs.cleanup();
|
|
3598
|
+
await loaded.release();
|
|
2697
3599
|
}
|
|
2698
3600
|
};
|
|
2699
3601
|
var findNearestFile = async (startDirectory, filename) => {
|
|
2700
3602
|
let current = startDirectory;
|
|
2701
3603
|
for (; ; ) {
|
|
2702
|
-
const candidate = (0,
|
|
3604
|
+
const candidate = (0, import_node_path6.join)(current, filename);
|
|
2703
3605
|
try {
|
|
2704
|
-
await (0,
|
|
3606
|
+
await (0, import_promises5.access)(candidate);
|
|
2705
3607
|
return candidate;
|
|
2706
3608
|
} catch {
|
|
2707
|
-
const parent = (0,
|
|
3609
|
+
const parent = (0, import_node_path6.dirname)(current);
|
|
2708
3610
|
if (parent === current) return void 0;
|
|
2709
3611
|
current = parent;
|
|
2710
3612
|
}
|
|
2711
3613
|
}
|
|
2712
3614
|
};
|
|
2713
3615
|
var registerTsconfigPaths = async (tsconfigPath) => {
|
|
2714
|
-
const
|
|
3616
|
+
const parsed = import_typescript2.default.parseConfigFileTextToJson(
|
|
3617
|
+
tsconfigPath,
|
|
3618
|
+
await (0, import_promises5.readFile)(tsconfigPath, "utf8")
|
|
3619
|
+
);
|
|
3620
|
+
if (parsed.error)
|
|
3621
|
+
throw new Error(`Invalid TypeScript configuration: ${tsconfigPath}`);
|
|
3622
|
+
const raw = parsed.config;
|
|
2715
3623
|
if (!isRecord(raw)) return () => void 0;
|
|
2716
3624
|
const compilerOptions = isRecord(raw.compilerOptions) ? raw.compilerOptions : void 0;
|
|
2717
3625
|
const paths = isRecord(compilerOptions?.paths) ? compilerOptions.paths : void 0;
|
|
2718
3626
|
if (!paths) return () => void 0;
|
|
2719
|
-
const tsconfigDirectory = (0,
|
|
2720
|
-
const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0,
|
|
3627
|
+
const tsconfigDirectory = (0, import_node_path6.dirname)(tsconfigPath);
|
|
3628
|
+
const baseUrl = typeof compilerOptions?.baseUrl === "string" ? (0, import_node_path6.resolve)(tsconfigDirectory, compilerOptions.baseUrl) : tsconfigDirectory;
|
|
2721
3629
|
const mappings = Object.entries(paths).flatMap(([pattern, rawTargets]) => {
|
|
2722
3630
|
if (!Array.isArray(rawTargets)) return [];
|
|
2723
3631
|
return rawTargets.filter((target) => typeof target === "string").map((target) => ({ pattern, target }));
|
|
@@ -2746,7 +3654,7 @@ var registerTsconfigPaths = async (tsconfigPath) => {
|
|
|
2746
3654
|
request.length - mapping.patternSuffix.length
|
|
2747
3655
|
) : "";
|
|
2748
3656
|
const mapped = mapping.wildcard ? `${mapping.targetPrefix}${wildcardValue}${mapping.targetSuffix}` : mapping.target;
|
|
2749
|
-
for (const candidate of candidateModulePaths((0,
|
|
3657
|
+
for (const candidate of candidateModulePaths((0, import_node_path6.resolve)(baseUrl, mapped))) {
|
|
2750
3658
|
try {
|
|
2751
3659
|
return originalResolve.call(this, candidate, parent, isMain, options);
|
|
2752
3660
|
} catch {
|
|
@@ -2767,10 +3675,10 @@ var candidateModulePaths = (basePath) => [
|
|
|
2767
3675
|
`${basePath}.cts`,
|
|
2768
3676
|
`${basePath}.js`,
|
|
2769
3677
|
`${basePath}.jsx`,
|
|
2770
|
-
(0,
|
|
2771
|
-
(0,
|
|
2772
|
-
(0,
|
|
2773
|
-
(0,
|
|
3678
|
+
(0, import_node_path6.join)(basePath, "index.ts"),
|
|
3679
|
+
(0, import_node_path6.join)(basePath, "index.tsx"),
|
|
3680
|
+
(0, import_node_path6.join)(basePath, "index.js"),
|
|
3681
|
+
(0, import_node_path6.join)(basePath, "index.jsx")
|
|
2774
3682
|
];
|
|
2775
3683
|
var valueFromModule = (module2, exportName) => {
|
|
2776
3684
|
if (exportName) {
|
|
@@ -2800,7 +3708,7 @@ var projectSnapshots = async (value, options) => {
|
|
|
2800
3708
|
}
|
|
2801
3709
|
const knownWorkflowIds = workflows.map((workflow) => workflow.id);
|
|
2802
3710
|
return workflows.map(
|
|
2803
|
-
(workflow) => (0,
|
|
3711
|
+
(workflow) => (0, import_agent2.projectWorkflowTopology)({
|
|
2804
3712
|
workflow,
|
|
2805
3713
|
environment: options.environment,
|
|
2806
3714
|
service: options.service,
|
|
@@ -2928,12 +3836,30 @@ var runTopologyPush = async (rawOptions) => {
|
|
|
2928
3836
|
printResults(results, options);
|
|
2929
3837
|
};
|
|
2930
3838
|
var createCliProgram = () => {
|
|
2931
|
-
const program = new
|
|
3839
|
+
const program = new import_commander6.Command().name("kortyx").description("Build, observe, and operate Kortyx projects.").showHelpAfterError().addHelpText(
|
|
2932
3840
|
"after",
|
|
2933
3841
|
'\nRun "kortyx <command> --help" for detailed command options.'
|
|
2934
3842
|
);
|
|
2935
3843
|
program.addCommand(createStudioCommand());
|
|
2936
3844
|
program.addCommand(createConnectionsCommand());
|
|
3845
|
+
let releaseEvalEntry;
|
|
3846
|
+
program.addCommand(
|
|
3847
|
+
createLocalEvalsCommand({
|
|
3848
|
+
load: async (entry, envFiles) => {
|
|
3849
|
+
Object.assign(
|
|
3850
|
+
process.env,
|
|
3851
|
+
await loadEvalEnvironment(process.cwd(), envFiles)
|
|
3852
|
+
);
|
|
3853
|
+
const loaded = await openEntry(entry);
|
|
3854
|
+
releaseEvalEntry = loaded.release;
|
|
3855
|
+
return loaded.module;
|
|
3856
|
+
},
|
|
3857
|
+
release: async () => {
|
|
3858
|
+
await releaseEvalEntry?.();
|
|
3859
|
+
releaseEvalEntry = void 0;
|
|
3860
|
+
}
|
|
3861
|
+
})
|
|
3862
|
+
);
|
|
2937
3863
|
const topology = program.command("topology").description("Publish deterministic workflow topology to Studio.");
|
|
2938
3864
|
topology.command("push").description("Project and publish workflow topology.").requiredOption(
|
|
2939
3865
|
"--entry <path>",
|
|
@@ -2971,7 +3897,7 @@ var main = async () => {
|
|
|
2971
3897
|
try {
|
|
2972
3898
|
await program.parseAsync(process.argv);
|
|
2973
3899
|
} catch (error) {
|
|
2974
|
-
if (error instanceof
|
|
3900
|
+
if (error instanceof import_commander6.CommanderError && error.exitCode === 0) return;
|
|
2975
3901
|
if (error instanceof StudioReadError) {
|
|
2976
3902
|
console.error(
|
|
2977
3903
|
process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`
|