@huydao/karrot 0.1.1
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/GUIDE.md +484 -0
- package/README.md +253 -0
- package/dist/assertions/assertion.d.ts +18 -0
- package/dist/assertions/assertion.js +198 -0
- package/dist/assertions/turn-eval.d.ts +22 -0
- package/dist/assertions/turn-eval.js +178 -0
- package/dist/executors/adapters/ag-ui-post.d.ts +55 -0
- package/dist/executors/adapters/ag-ui-post.js +703 -0
- package/dist/executors/adapters/ag-ui.d.ts +15 -0
- package/dist/executors/adapters/ag-ui.js +275 -0
- package/dist/executors/execute.d.ts +16 -0
- package/dist/executors/execute.js +145 -0
- package/dist/executors/executor.d.ts +37 -0
- package/dist/executors/executor.js +203 -0
- package/dist/executors/run-result.d.ts +33 -0
- package/dist/executors/run-result.js +22 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +28 -0
- package/dist/prompts/turn-eval-system-prompt.md +68 -0
- package/dist/prompts/turn-message-gen-system-prompt.md +16 -0
- package/dist/reports/report.d.ts +68 -0
- package/dist/reports/report.js +366 -0
- package/dist/scenarios/generated-message.d.ts +15 -0
- package/dist/scenarios/generated-message.js +116 -0
- package/dist/scenarios/scenario-loader.d.ts +12 -0
- package/dist/scenarios/scenario-loader.js +103 -0
- package/dist/scenarios/scenario.d.ts +62 -0
- package/dist/scenarios/scenario.js +35 -0
- package/dist/utils/artifact-files.d.ts +3 -0
- package/dist/utils/artifact-files.js +22 -0
- package/dist/utils/config.d.ts +101 -0
- package/dist/utils/config.js +57 -0
- package/dist/utils/openai-eval.d.ts +5 -0
- package/dist/utils/openai-eval.js +54 -0
- package/package.json +146 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MessageRunResult } from '../run-result';
|
|
2
|
+
type RunAgUiMessageOptions = {
|
|
3
|
+
message: string;
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
outputDirectory: string;
|
|
6
|
+
threadId?: string;
|
|
7
|
+
threadIdFallback?: string;
|
|
8
|
+
allowIdleTimeoutWithAssistantText?: boolean;
|
|
9
|
+
processTimeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseExecutionTestResultId(output: string): string | undefined;
|
|
12
|
+
export declare function extractToolCallNames(logContent: string): string[];
|
|
13
|
+
export declare function extractAppendedLog(previousLogContent: string, latestLogContent: string): string;
|
|
14
|
+
export declare function runAgUiMessage(options: RunAgUiMessageOptions): Promise<MessageRunResult>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseExecutionTestResultId = parseExecutionTestResultId;
|
|
7
|
+
exports.extractToolCallNames = extractToolCallNames;
|
|
8
|
+
exports.extractAppendedLog = extractAppendedLog;
|
|
9
|
+
exports.runAgUiMessage = runAgUiMessage;
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const run_result_1 = require("../run-result");
|
|
14
|
+
function getAgUiBinaryPath() {
|
|
15
|
+
const packageJsonPath = require.resolve('ag-ui-wss/package.json');
|
|
16
|
+
const packageDirectory = node_path_1.default.dirname(packageJsonPath);
|
|
17
|
+
const packageJson = JSON.parse(require('node:fs').readFileSync(packageJsonPath, 'utf8'));
|
|
18
|
+
const binaryRelativePath = packageJson.bin?.['ag-ui-wss'];
|
|
19
|
+
if (!binaryRelativePath) {
|
|
20
|
+
throw new Error(`Unable to resolve ag-ui-wss binary from ${packageJsonPath}.`);
|
|
21
|
+
}
|
|
22
|
+
return node_path_1.default.join(packageDirectory, binaryRelativePath);
|
|
23
|
+
}
|
|
24
|
+
function parseThreadId(output) {
|
|
25
|
+
return output.match(/Thread:\s*([^\s]+)/)?.[1];
|
|
26
|
+
}
|
|
27
|
+
function extractStdoutAssistantText(output) {
|
|
28
|
+
const normalized = output.replace(/\r/g, '');
|
|
29
|
+
if (!normalized.trim()) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const withoutAnsi = normalized.replace(/\u001B\[[0-9;]*m/g, '');
|
|
33
|
+
const trimmed = withoutAnsi.trim();
|
|
34
|
+
return trimmed || undefined;
|
|
35
|
+
}
|
|
36
|
+
function parseExecutionTestResultId(output) {
|
|
37
|
+
const urlMatch = output.match(/\/test-results\/(\d+)/);
|
|
38
|
+
if (urlMatch) {
|
|
39
|
+
return urlMatch[1];
|
|
40
|
+
}
|
|
41
|
+
const plainMatch = output.match(/test result(?: ID)?\s+(\d{3,})/i);
|
|
42
|
+
return plainMatch?.[1];
|
|
43
|
+
}
|
|
44
|
+
function parseTimingMetrics(output) {
|
|
45
|
+
const matches = [
|
|
46
|
+
...output.matchAll(/(?:(?:TTF-Tool:\s*([\d.]+)s)\s*\|\s*)?TTF-Text:\s*([\d.]+)s\s*\|\s*Total:\s*([\d.]+)s\s*\|\s*Protocol efficiency:\s*([\d.]+)KB\/([\d.]+)KB\s*\((\d+)%\)/g),
|
|
47
|
+
];
|
|
48
|
+
const match = matches.at(-1);
|
|
49
|
+
if (!match) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ttfToolSeconds: match[1] ? Number(match[1]) : undefined,
|
|
54
|
+
ttfTextSeconds: Number(match[2]),
|
|
55
|
+
totalSeconds: Number(match[3]),
|
|
56
|
+
protocolUsedKb: Number(match[4]),
|
|
57
|
+
protocolTotalKb: Number(match[5]),
|
|
58
|
+
efficiencyPercent: Number(match[6]),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseRunError(logContent) {
|
|
62
|
+
const lines = logContent
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
67
|
+
try {
|
|
68
|
+
const event = JSON.parse(lines[index]);
|
|
69
|
+
if (event.type === 'RUN_ERROR' && typeof event.error === 'string' && event.error.trim()) {
|
|
70
|
+
return event.error.trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore malformed lines in the JSONL stream.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function parseConsoleError(output) {
|
|
80
|
+
const matches = [...output.matchAll(/^Error:\s*(.+)$/gm)]
|
|
81
|
+
.map((match) => match[1].trim())
|
|
82
|
+
.filter((message) => message && message !== 'Agent run failed');
|
|
83
|
+
return matches.at(0);
|
|
84
|
+
}
|
|
85
|
+
function countToolCalls(logContent) {
|
|
86
|
+
return [...logContent.matchAll(/"type":"TOOL_CALL_START"/g)].length;
|
|
87
|
+
}
|
|
88
|
+
function extractToolCallNames(logContent) {
|
|
89
|
+
const toolCalls = [];
|
|
90
|
+
const lines = logContent
|
|
91
|
+
.split('\n')
|
|
92
|
+
.map((line) => line.trim())
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
try {
|
|
96
|
+
const event = JSON.parse(line);
|
|
97
|
+
if (event.type !== 'TOOL_CALL_START' || typeof event.toolCallName !== 'string') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const toolCallName = event.toolCallName.trim();
|
|
101
|
+
if (toolCallName) {
|
|
102
|
+
toolCalls.push(toolCallName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Ignore malformed JSONL lines.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return toolCalls;
|
|
110
|
+
}
|
|
111
|
+
function extractAppendedLog(previousLogContent, latestLogContent) {
|
|
112
|
+
if (!previousLogContent) {
|
|
113
|
+
return latestLogContent;
|
|
114
|
+
}
|
|
115
|
+
if (latestLogContent.startsWith(previousLogContent)) {
|
|
116
|
+
return latestLogContent.slice(previousLogContent.length);
|
|
117
|
+
}
|
|
118
|
+
return latestLogContent;
|
|
119
|
+
}
|
|
120
|
+
function extractAssistantText(logContent) {
|
|
121
|
+
const fragmentsByMessage = new Map();
|
|
122
|
+
const orderedMessageIds = [];
|
|
123
|
+
let latestFullContent;
|
|
124
|
+
const lines = logContent
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map((line) => line.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
try {
|
|
130
|
+
const event = JSON.parse(line);
|
|
131
|
+
if (!String(event.type).startsWith('TEXT_MESSAGE_')) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const messageId = typeof event.messageId === 'string' && event.messageId.trim()
|
|
135
|
+
? event.messageId.trim()
|
|
136
|
+
: '__default__';
|
|
137
|
+
if (!fragmentsByMessage.has(messageId)) {
|
|
138
|
+
fragmentsByMessage.set(messageId, []);
|
|
139
|
+
orderedMessageIds.push(messageId);
|
|
140
|
+
}
|
|
141
|
+
if (typeof event.content === 'string' && event.content.trim()) {
|
|
142
|
+
latestFullContent = event.content.trim();
|
|
143
|
+
}
|
|
144
|
+
if (typeof event.text === 'string' && event.text.trim()) {
|
|
145
|
+
latestFullContent = event.text.trim();
|
|
146
|
+
}
|
|
147
|
+
if (typeof event.delta === 'string' && event.delta.length > 0) {
|
|
148
|
+
fragmentsByMessage.get(messageId)?.push(event.delta);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore malformed JSONL lines.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (latestFullContent) {
|
|
156
|
+
return latestFullContent;
|
|
157
|
+
}
|
|
158
|
+
const lastMessageId = orderedMessageIds.at(-1);
|
|
159
|
+
if (!lastMessageId) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const joined = fragmentsByMessage.get(lastMessageId)?.join('').trim();
|
|
163
|
+
return joined || undefined;
|
|
164
|
+
}
|
|
165
|
+
async function readJsonl(pathname) {
|
|
166
|
+
try {
|
|
167
|
+
return await promises_1.default.readFile(pathname, 'utf8');
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function runAgUiMessage(options) {
|
|
174
|
+
await promises_1.default.mkdir(options.outputDirectory, { recursive: true });
|
|
175
|
+
const expectedThreadId = options.threadId;
|
|
176
|
+
const existingLogPath = expectedThreadId
|
|
177
|
+
? node_path_1.default.join(options.outputDirectory, `${expectedThreadId}.jsonl`)
|
|
178
|
+
: undefined;
|
|
179
|
+
const previousLogContent = existingLogPath ? await readJsonl(existingLogPath) : '';
|
|
180
|
+
const child = (0, node_child_process_1.spawn)(getAgUiBinaryPath(), expectedThreadId ? ['--thread', expectedThreadId, options.message] : [options.message], {
|
|
181
|
+
cwd: options.outputDirectory,
|
|
182
|
+
env: options.env,
|
|
183
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
+
});
|
|
185
|
+
let stdoutOutput = '';
|
|
186
|
+
let stderrOutput = '';
|
|
187
|
+
child.stdout.on('data', (chunk) => {
|
|
188
|
+
const text = chunk.toString();
|
|
189
|
+
stdoutOutput += text;
|
|
190
|
+
process.stdout.write(text);
|
|
191
|
+
});
|
|
192
|
+
child.stderr.on('data', (chunk) => {
|
|
193
|
+
const text = chunk.toString();
|
|
194
|
+
stderrOutput += text;
|
|
195
|
+
process.stderr.write(text);
|
|
196
|
+
});
|
|
197
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
198
|
+
let timedOut = false;
|
|
199
|
+
const timeoutId = typeof options.processTimeoutMs === 'number'
|
|
200
|
+
? setTimeout(() => {
|
|
201
|
+
timedOut = true;
|
|
202
|
+
child.kill('SIGTERM');
|
|
203
|
+
setTimeout(() => child.kill('SIGKILL'), 5_000).unref();
|
|
204
|
+
reject(new run_result_1.MessageRunError(`ag-ui-wss exceeded ${options.processTimeoutMs}ms and was terminated.`, {
|
|
205
|
+
threadId: parseThreadId(`${stderrOutput}\n${stdoutOutput}`) ?? expectedThreadId ?? options.threadIdFallback,
|
|
206
|
+
output: extractStdoutAssistantText(stdoutOutput) ?? '',
|
|
207
|
+
}));
|
|
208
|
+
}, options.processTimeoutMs)
|
|
209
|
+
: undefined;
|
|
210
|
+
child.on('error', reject);
|
|
211
|
+
child.on('close', (code) => {
|
|
212
|
+
if (timeoutId) {
|
|
213
|
+
clearTimeout(timeoutId);
|
|
214
|
+
}
|
|
215
|
+
if (timedOut) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
resolve(code ?? 1);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
const combinedConsoleOutput = `${stderrOutput}\n${stdoutOutput}`;
|
|
222
|
+
const threadId = parseThreadId(combinedConsoleOutput) ?? expectedThreadId ?? options.threadIdFallback;
|
|
223
|
+
if (!threadId) {
|
|
224
|
+
throw new Error('Unable to parse the thread ID from ag-ui-wss output.');
|
|
225
|
+
}
|
|
226
|
+
const logPath = node_path_1.default.join(options.outputDirectory, `${threadId}.jsonl`);
|
|
227
|
+
const logContent = await readJsonl(logPath);
|
|
228
|
+
const runLogContent = extractAppendedLog(previousLogContent, logContent);
|
|
229
|
+
const assistantOutput = extractAssistantText(runLogContent)
|
|
230
|
+
?? extractAssistantText(logContent)
|
|
231
|
+
?? extractStdoutAssistantText(stdoutOutput)
|
|
232
|
+
?? '';
|
|
233
|
+
const metrics = parseTimingMetrics(combinedConsoleOutput);
|
|
234
|
+
const toolCallCount = countToolCalls(runLogContent);
|
|
235
|
+
const toolCalls = extractToolCallNames(runLogContent);
|
|
236
|
+
const hasAssistantTextEvent = /"type":"TEXT_MESSAGE_CONTENT"/.test(runLogContent)
|
|
237
|
+
|| /"type":"TEXT_MESSAGE_CONTENT"/.test(logContent);
|
|
238
|
+
const missingAssistantTextNote = !assistantOutput.trim() && !hasAssistantTextEvent
|
|
239
|
+
? 'Run finished without any assistant text content.'
|
|
240
|
+
: undefined;
|
|
241
|
+
if (exitCode !== 0) {
|
|
242
|
+
const hasRunError = runLogContent.includes('"type":"RUN_ERROR"');
|
|
243
|
+
const hasAssistantText = hasAssistantTextEvent || Boolean(extractStdoutAssistantText(stdoutOutput));
|
|
244
|
+
if (!hasRunError &&
|
|
245
|
+
hasAssistantText &&
|
|
246
|
+
combinedConsoleOutput.includes('idle timeout')) {
|
|
247
|
+
return {
|
|
248
|
+
output: assistantOutput,
|
|
249
|
+
threadId,
|
|
250
|
+
outputPath: logPath,
|
|
251
|
+
note: 'Assistant text received but the run did not emit RUN_FINISHED before idle timeout.',
|
|
252
|
+
toolCallCount,
|
|
253
|
+
toolCalls,
|
|
254
|
+
metrics,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
throw new run_result_1.MessageRunError(parseRunError(runLogContent) ?? parseConsoleError(combinedConsoleOutput) ?? `ag-ui-wss exited with code ${exitCode}.`, {
|
|
258
|
+
threadId,
|
|
259
|
+
outputPath: logPath,
|
|
260
|
+
output: assistantOutput,
|
|
261
|
+
metrics,
|
|
262
|
+
toolCallCount,
|
|
263
|
+
toolCalls,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
output: assistantOutput,
|
|
268
|
+
threadId,
|
|
269
|
+
outputPath: logPath,
|
|
270
|
+
note: missingAssistantTextNote,
|
|
271
|
+
toolCallCount,
|
|
272
|
+
toolCalls,
|
|
273
|
+
metrics,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type KarrotConfig, type KarrotScenarioSelection } from '../utils/config';
|
|
2
|
+
import { type ScenarioRunResult } from '../reports/report';
|
|
3
|
+
type ExecuteOptions = {
|
|
4
|
+
variables?: Record<string, unknown>;
|
|
5
|
+
scenario: KarrotScenarioSelection;
|
|
6
|
+
};
|
|
7
|
+
type ExecuteResult = {
|
|
8
|
+
outputDirectory: string;
|
|
9
|
+
results: ScenarioRunResult[];
|
|
10
|
+
reportPaths?: {
|
|
11
|
+
jsonPath: string;
|
|
12
|
+
htmlPath: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function execute(configOrPath: KarrotConfig | string, options: ExecuteOptions): Promise<ExecuteResult>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.execute = execute;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const artifact_files_1 = require("../utils/artifact-files");
|
|
9
|
+
const ag_ui_post_1 = require("./adapters/ag-ui-post");
|
|
10
|
+
const ag_ui_1 = require("./adapters/ag-ui");
|
|
11
|
+
const config_1 = require("../utils/config");
|
|
12
|
+
const executor_1 = require("./executor");
|
|
13
|
+
const report_1 = require("../reports/report");
|
|
14
|
+
const scenario_loader_1 = require("../scenarios/scenario-loader");
|
|
15
|
+
function resolveConfiguredPath(baseDirectory, value) {
|
|
16
|
+
const trimmed = value?.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return node_path_1.default.isAbsolute(trimmed) ? trimmed : node_path_1.default.resolve(baseDirectory, trimmed);
|
|
21
|
+
}
|
|
22
|
+
function createAgUiRunner(config) {
|
|
23
|
+
const transport = config.transport;
|
|
24
|
+
if (transport.type !== 'ag-ui-wss') {
|
|
25
|
+
throw new Error('createAgUiRunner requires ag-ui-wss transport config.');
|
|
26
|
+
}
|
|
27
|
+
return async ({ message, outputDirectory, threadId, processTimeoutMs }) => await (0, ag_ui_1.runAgUiMessage)({
|
|
28
|
+
message,
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
...transport.env,
|
|
32
|
+
},
|
|
33
|
+
outputDirectory,
|
|
34
|
+
threadId,
|
|
35
|
+
processTimeoutMs: processTimeoutMs ?? transport.processTimeoutMs,
|
|
36
|
+
allowIdleTimeoutWithAssistantText: transport.allowIdleTimeoutWithAssistantText,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function createAgUiPostRunner(config) {
|
|
40
|
+
const transport = config.transport;
|
|
41
|
+
if (transport.type !== 'ag-ui-post') {
|
|
42
|
+
throw new Error('createAgUiPostRunner requires ag-ui-post transport config.');
|
|
43
|
+
}
|
|
44
|
+
return async ({ message, outputDirectory, threadId, processTimeoutMs }) => await (0, ag_ui_post_1.runAgUiPostMessage)({
|
|
45
|
+
message,
|
|
46
|
+
outputDirectory,
|
|
47
|
+
threadId,
|
|
48
|
+
processTimeoutMs: processTimeoutMs ?? transport.processTimeoutMs,
|
|
49
|
+
injectMessage: transport.injectMessage,
|
|
50
|
+
injectRunMetadata: transport.injectRunMetadata,
|
|
51
|
+
run: transport.run ?? transport.request,
|
|
52
|
+
connect: transport.connect,
|
|
53
|
+
observe: transport.observe,
|
|
54
|
+
completionCheck: transport.completionCheck,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function isAgUiWssConfig(config) {
|
|
58
|
+
return config.transport.type === 'ag-ui-wss';
|
|
59
|
+
}
|
|
60
|
+
function isAgUiPostConfig(config) {
|
|
61
|
+
return config.transport.type === 'ag-ui-post';
|
|
62
|
+
}
|
|
63
|
+
function normalizeConfig(config) {
|
|
64
|
+
if (config.version !== 1) {
|
|
65
|
+
throw new Error(`Unsupported karrot config version "${String(config.version)}".`);
|
|
66
|
+
}
|
|
67
|
+
if (!isAgUiWssConfig(config) && !isAgUiPostConfig(config)) {
|
|
68
|
+
throw new Error(`Unsupported transport type "${config.transport.type}".`);
|
|
69
|
+
}
|
|
70
|
+
if (!config.context.projectId || typeof config.context.projectId !== 'string') {
|
|
71
|
+
throw new Error('karrot config requires context.projectId.');
|
|
72
|
+
}
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
function normalizeScenarioSelection(selection) {
|
|
76
|
+
if (!selection.file?.trim()) {
|
|
77
|
+
throw new Error('execute requires scenario.file.');
|
|
78
|
+
}
|
|
79
|
+
return selection;
|
|
80
|
+
}
|
|
81
|
+
async function execute(configOrPath, options) {
|
|
82
|
+
const configBaseDirectory = typeof configOrPath === 'string'
|
|
83
|
+
? node_path_1.default.dirname(node_path_1.default.isAbsolute(configOrPath) ? configOrPath : node_path_1.default.resolve(process.cwd(), configOrPath))
|
|
84
|
+
: process.cwd();
|
|
85
|
+
const loadedConfig = typeof configOrPath === 'string'
|
|
86
|
+
? await (0, config_1.loadConfig)(configOrPath)
|
|
87
|
+
: configOrPath;
|
|
88
|
+
const resolvedConfig = normalizeConfig(options.variables ? (0, config_1.resolveVariables)(loadedConfig, options.variables) : loadedConfig);
|
|
89
|
+
const scenarioSelection = normalizeScenarioSelection(options.variables ? (0, config_1.resolveVariables)(options.scenario, options.variables) : options.scenario);
|
|
90
|
+
const outputDirectory = await (0, artifact_files_1.createRunArtifactDirectory)(resolvedConfig.artifacts?.directory);
|
|
91
|
+
const scenarioModule = await (0, scenario_loader_1.loadScenarioModule)({
|
|
92
|
+
scenarioFile: scenarioSelection.file,
|
|
93
|
+
defaultRelativePath: scenarioSelection.file,
|
|
94
|
+
});
|
|
95
|
+
const context = {
|
|
96
|
+
...scenarioModule.buildScenarioContext(resolvedConfig.context.projectId),
|
|
97
|
+
...resolvedConfig.context,
|
|
98
|
+
};
|
|
99
|
+
const selectedScenarios = scenarioModule.scenarioSet.select(scenarioSelection.ids);
|
|
100
|
+
if (selectedScenarios.length === 0) {
|
|
101
|
+
throw new Error('No matching scenarios were selected.');
|
|
102
|
+
}
|
|
103
|
+
const results = await (0, executor_1.runScenario)(selectedScenarios, {
|
|
104
|
+
context,
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
...(resolvedConfig.evaluation?.systemPromptPath
|
|
108
|
+
? {
|
|
109
|
+
AI_TURN_EVAL_SYSTEM_PROMPT_PATH: resolveConfiguredPath(configBaseDirectory, resolvedConfig.evaluation.systemPromptPath),
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
...(resolvedConfig.evaluation?.promptDirectory
|
|
113
|
+
? {
|
|
114
|
+
AI_TURN_EVAL_PROMPT_DIRECTORY: resolveConfiguredPath(configBaseDirectory, resolvedConfig.evaluation.promptDirectory),
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(isAgUiWssConfig(resolvedConfig) ? resolvedConfig.transport.env : {}),
|
|
118
|
+
},
|
|
119
|
+
outputDirectory,
|
|
120
|
+
messageRunner: isAgUiWssConfig(resolvedConfig)
|
|
121
|
+
? createAgUiRunner(resolvedConfig)
|
|
122
|
+
: createAgUiPostRunner(resolvedConfig),
|
|
123
|
+
stopOnFailure: resolvedConfig.execution?.stopOnFailure ?? false,
|
|
124
|
+
});
|
|
125
|
+
let reportPaths;
|
|
126
|
+
if (resolvedConfig.report?.enabled !== false && resolvedConfig.report) {
|
|
127
|
+
reportPaths = await (0, report_1.writeScenarioRunReport)({
|
|
128
|
+
outputDirectory,
|
|
129
|
+
runtime: resolvedConfig.report.runtime,
|
|
130
|
+
environment: resolvedConfig.report.environment,
|
|
131
|
+
projectName: resolvedConfig.report.projectName,
|
|
132
|
+
scenarioContext: {
|
|
133
|
+
...context,
|
|
134
|
+
...resolvedConfig.report.scenarioContext,
|
|
135
|
+
scenarioFilePath: scenarioModule.scenarioFilePath,
|
|
136
|
+
},
|
|
137
|
+
results,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
outputDirectory,
|
|
142
|
+
results,
|
|
143
|
+
reportPaths,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type AiScenario, type AiTurn, type BaseAiScenarioContext } from '../scenarios/scenario';
|
|
2
|
+
import { type ScenarioRunResult } from '../reports/report';
|
|
3
|
+
import { type MessageRunResult } from './run-result';
|
|
4
|
+
type BaseScenarioExecutionOptions = {
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
outputDirectory: string;
|
|
7
|
+
stopOnFailure?: boolean;
|
|
8
|
+
maxDurationMs?: number;
|
|
9
|
+
initialThreadId?: string;
|
|
10
|
+
};
|
|
11
|
+
export type ScenarioMessageRunner<TContext extends BaseAiScenarioContext> = (options: {
|
|
12
|
+
scenario: AiScenario<TContext>;
|
|
13
|
+
turn: AiTurn<TContext>;
|
|
14
|
+
context: TContext;
|
|
15
|
+
message: string;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
outputDirectory: string;
|
|
18
|
+
threadId?: string;
|
|
19
|
+
processTimeoutMs?: number;
|
|
20
|
+
}) => Promise<MessageRunResult>;
|
|
21
|
+
export declare function runScenario(scenario: AiScenario<BaseAiScenarioContext>, options: BaseScenarioExecutionOptions & {
|
|
22
|
+
context: BaseAiScenarioContext;
|
|
23
|
+
messageRunner?: ScenarioMessageRunner<BaseAiScenarioContext>;
|
|
24
|
+
}): Promise<ScenarioRunResult>;
|
|
25
|
+
export declare function runScenario(scenario: AiScenario<BaseAiScenarioContext>[], options: BaseScenarioExecutionOptions & {
|
|
26
|
+
context: BaseAiScenarioContext;
|
|
27
|
+
messageRunner?: ScenarioMessageRunner<BaseAiScenarioContext>;
|
|
28
|
+
}): Promise<ScenarioRunResult[]>;
|
|
29
|
+
export declare function runScenario<TContext extends BaseAiScenarioContext>(scenario: AiScenario<TContext>, options: BaseScenarioExecutionOptions & {
|
|
30
|
+
context: TContext;
|
|
31
|
+
messageRunner?: ScenarioMessageRunner<TContext>;
|
|
32
|
+
}): Promise<ScenarioRunResult>;
|
|
33
|
+
export declare function runScenario<TContext extends BaseAiScenarioContext>(scenario: AiScenario<TContext>[], options: BaseScenarioExecutionOptions & {
|
|
34
|
+
context: TContext;
|
|
35
|
+
messageRunner?: ScenarioMessageRunner<TContext>;
|
|
36
|
+
}): Promise<ScenarioRunResult[]>;
|
|
37
|
+
export {};
|