@alpic-ai/tracks 0.5.1 → 0.7.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/dist/main.js +686 -66
- package/package.json +6 -5
- package/skills/agent-experience-evals/README.md +40 -0
- package/skills/agent-experience-evals/SKILL.md +95 -0
- package/skills/agent-experience-evals/evals/evals.json +90 -0
- package/skills/agent-experience-evals/evals/files/README.md +7 -0
- package/skills/agent-experience-evals/evals/files/example/SKILL.md +6 -0
- package/skills/agent-experience-evals/evals/files/example/evals/evals.json +12 -0
- package/skills/agent-experience-evals/evals/files/example/evals/targets.json +3 -0
- package/skills/agent-experience-evals/evals/targets.json +4 -0
- package/skills/agent-experience-evals/report.html +149 -0
- package/skills/agent-experience-evals/scripts/evals.mjs +405 -0
- package/skills/agent-experience-evals/scripts/report.mjs +61 -0
package/dist/main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
3
|
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, OpenApi } from "effect/http-api";
|
|
4
|
-
import { Config, Console, Context, Deferred, Effect, Layer, Option, Redacted, Result, Schema, SchemaTransformation, Stream, Struct } from "effect";
|
|
5
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
+
import { Config, Console, Context, Deferred, Effect, FileSystem, Layer, Option, Path, Redacted, Result, Schedule, Schema, SchemaTransformation, Stream, Struct } from "effect";
|
|
5
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
6
6
|
import { isIPv4 } from "node:net";
|
|
7
|
-
import { Argument, Command, Flag } from "effect/cli";
|
|
7
|
+
import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
10
10
|
Context.Service()("@tracks/contract/RequestOrganization");
|
|
@@ -30,8 +30,7 @@ const List = (item) => Schema.Struct({
|
|
|
30
30
|
data: Schema.Array(item),
|
|
31
31
|
has_more: Schema.Boolean.annotate({ description: "Whether more items are available after this page." })
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
//#region ../contract/src/validation.ts
|
|
33
|
+
// ../contract/src/validation.ts
|
|
35
34
|
const RequestIssue = Schema.Struct({
|
|
36
35
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Number])).annotate({ description: "Keys leading to the wrong value inside that part of the request, such as `[\"agent\", \"model\"]`. Empty when the problem concerns that part as a whole." }),
|
|
37
36
|
message: Schema.String.annotate({ description: "What is wrong with the value." })
|
|
@@ -53,8 +52,7 @@ var InvalidRequest = class extends Schema.TaggedError()("InvalidRequest", {
|
|
|
53
52
|
description: "The request is invalid: a value is missing, has the wrong type or is out of range. Fix it before sending the request again."
|
|
54
53
|
}) {};
|
|
55
54
|
var RequestValidation = class extends HttpApiMiddleware.Service()("@tracks/contract/RequestValidation", { error: InvalidRequest }) {};
|
|
56
|
-
|
|
57
|
-
//#region ../contract/src/connections.ts
|
|
55
|
+
// ../contract/src/connections.ts
|
|
58
56
|
const ConnectionId = Schema.String.pipe(Schema.brand("ConnectionId")).annotate({
|
|
59
57
|
identifier: "ConnectionId",
|
|
60
58
|
description: "Unique identifier of the connection."
|
|
@@ -114,7 +112,7 @@ var ConnectionsApi = class extends HttpApiGroup.make("connections").add(HttpApiE
|
|
|
114
112
|
error: [ConnectionRefused, ServerUnreachable]
|
|
115
113
|
}).annotateMerge(OpenApi.annotations({
|
|
116
114
|
summary: "Create a connection",
|
|
117
|
-
description: "Starts connecting to an MCP server that acts as one of its users. Send the user's browser to `consent_url`. Once they consent, they get a `confirmation`: as a code shown to them, to give back to you, or, if you passed the optional `return_url`, sent to it with `connection_id`. Confirm the connection with it within 10 minutes to make it `active`, then give its
|
|
115
|
+
description: "Starts connecting to an MCP server that acts as one of its users. Send the user's browser to `consent_url`. Once they consent, they get a `confirmation`: as a code shown to them, to give back to you, or, if you passed the optional `return_url`, sent to it with `connection_id`. Confirm the connection with it within 10 minutes to make it `active`, then give its `id` to runs as `connection_id`."
|
|
118
116
|
})), HttpApiEndpoint.get("list", "/connections", { success: List(Connection) }).annotateMerge(OpenApi.annotations({
|
|
119
117
|
summary: "List connections",
|
|
120
118
|
description: "Returns every connection."
|
|
@@ -144,22 +142,62 @@ var ConnectionsApi = class extends HttpApiGroup.make("connections").add(HttpApiE
|
|
|
144
142
|
title: "Connections",
|
|
145
143
|
description: "Let agents use MCP servers as one of their users, without their credentials entering the sandbox."
|
|
146
144
|
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
// ../contract/src/files.ts
|
|
146
|
+
const FileId = Schema.String.check(Schema.isPattern(/^[0-9a-f-]{36}$/)).pipe(Schema.brand("FileId")).annotate({ description: "Reference returned when requesting a file upload." });
|
|
147
|
+
const FileName = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._ -]{0,127}$/)).annotate({ description: "Filename, without directories; up to 128 ASCII letters, digits, spaces, dots, hyphens or underscores, starting with a letter or digit." });
|
|
148
|
+
const FileContentType = Schema.String.check(Schema.isPattern(/^[\w!#$&^.+-]{1,127}\/[\w!#$&^.+-]{1,127}$/)).annotate({ description: "MIME type, without parameters; `application/octet-stream` when unknown." });
|
|
149
|
+
const FileSize = Schema.Int.check(Schema.isBetween({
|
|
150
|
+
minimum: 1,
|
|
151
|
+
maximum: 20971520
|
|
152
|
+
})).annotate({ description: "Exact size in bytes, from 1 to 20 MiB." });
|
|
153
|
+
const CreateUploadInput = Schema.Struct({
|
|
154
|
+
name: FileName,
|
|
155
|
+
content_type: FileContentType,
|
|
156
|
+
size: FileSize
|
|
157
|
+
});
|
|
158
|
+
const InputFile = Schema.Struct({
|
|
159
|
+
id: FileId,
|
|
160
|
+
name: FileName,
|
|
161
|
+
content_type: FileContentType,
|
|
162
|
+
size: FileSize,
|
|
163
|
+
path: Schema.String.annotate({ description: "Absolute path in the agent's sandbox." }),
|
|
164
|
+
download_url: Schema.String.annotate({ description: "Signed download URL, valid for 15 minutes." }),
|
|
165
|
+
download_expires_at: Schema.DateTimeUtc.annotate({ description: "When the download URL expires; fetch the run again to get a fresh link." })
|
|
166
|
+
});
|
|
167
|
+
var FileUnavailable = class extends Schema.TaggedError()("FileUnavailable", { message: Schema.String }, {
|
|
168
|
+
httpApiStatus: 422,
|
|
169
|
+
description: "An input file is missing, expired, or inaccessible to your organization."
|
|
170
|
+
}) {};
|
|
171
|
+
var FilesApi = class extends HttpApiGroup.make("files").add(HttpApiEndpoint.post("createUpload", "/files", {
|
|
172
|
+
payload: CreateUploadInput,
|
|
173
|
+
success: Schema.Struct({
|
|
174
|
+
id: FileId,
|
|
175
|
+
url: Schema.String,
|
|
176
|
+
method: Schema.Literal("PUT"),
|
|
177
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
178
|
+
upload_expires_at: Schema.DateTimeUtc,
|
|
179
|
+
expires_at: Schema.DateTimeUtc
|
|
180
|
+
}).pipe(HttpApiSchema.status(201))
|
|
181
|
+
}).annotateMerge(OpenApi.annotations({
|
|
182
|
+
summary: "Request a file upload",
|
|
183
|
+
description: "Request with `name`, `content_type` and exact byte `size`, e.g. `{\"name\":\"photo.png\",\"content_type\":\"image/png\",\"size\":12345}`. Any type of file, up to 20 MiB each. PUT raw bytes (not JSON or base64) to the returned `url` with every returned header within 15 minutes; browsers set `content-length` from the body. Uploads are write-once; repeating the PUT returns 412. Pass the returned `id` in `input_files` when creating a run. Files in the landing zone expire after 24 hours and are deleted asynchronously. Files attached to a run are retained permanently and their references can be reused by your organization."
|
|
184
|
+
}))).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
185
|
+
// ../contract/src/runs.ts
|
|
149
186
|
const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
|
|
150
187
|
identifier: "RunId",
|
|
151
188
|
description: "Unique identifier of the run."
|
|
152
189
|
});
|
|
153
190
|
const Harness = Schema.Literals([
|
|
154
191
|
"claude-code",
|
|
192
|
+
"claude-desktop",
|
|
155
193
|
"codex",
|
|
156
194
|
"codex-app",
|
|
157
195
|
"cursor"
|
|
158
196
|
]).annotate({
|
|
159
197
|
identifier: "Harness",
|
|
160
|
-
description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI), `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools) or `cursor` (Cursor's CLI)."
|
|
198
|
+
description: "The coding agent to run: `claude-code` (Claude Code), `claude-desktop` (Cowork, the agent workspace of Anthropic's Claude desktop app, for documents, research and longer tasks), `codex` (OpenAI's Codex CLI), `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools) or `cursor` (Cursor's CLI)."
|
|
161
199
|
});
|
|
162
|
-
const Agent = Schema.Struct({
|
|
200
|
+
const Agent$1 = Schema.Struct({
|
|
163
201
|
harness: Harness,
|
|
164
202
|
model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`, or a Cursor model id such as `gpt-5.6-luna-low`." })
|
|
165
203
|
}).annotate({
|
|
@@ -198,9 +236,14 @@ const MCP_SERVER_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
198
236
|
const MAX_MCP_SERVERS = 20;
|
|
199
237
|
const RESERVED_MCP_SERVER_NAMES = /* @__PURE__ */ new Set(["chatgpt_plugins"]);
|
|
200
238
|
const McpServerName = Schema.String.check(Schema.isPattern(MCP_SERVER_NAME)).annotate({ description: "Name the agent knows the MCP server by, unique in the run, matching `^[A-Za-z0-9_-]{1,64}$`, such as `linear`. `chatgpt_plugins` is reserved for running the agent." });
|
|
239
|
+
const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
|
|
240
|
+
const MAX_HEADERS = 20;
|
|
241
|
+
const MAX_HEADER_VALUE_LENGTH = 8192;
|
|
242
|
+
const McpServerHeaders = Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(MAX_HEADER_VALUE_LENGTH), Schema.isPattern(/^[^\r\n]*$/))).annotate({ description: `Headers we add to every request to the MCP server, typically its API key, such as \`{ "Authorization": "Bearer …" }\` or \`{ "x-api-key": "…" }\`. They never enter the sandbox and their values are never returned. Names match \`${HEADER_NAME.source}\`; at most ${MAX_HEADERS} headers of up to ${MAX_HEADER_VALUE_LENGTH} characters each, on one line.` }).check(Schema.isMinProperties(1), Schema.isMaxProperties(MAX_HEADERS), Schema.isPropertyNames(Schema.String.check(Schema.isPattern(HEADER_NAME))));
|
|
201
243
|
const McpServerInput = Schema.Union([Schema.Struct({
|
|
202
244
|
name: McpServerName,
|
|
203
|
-
url: HttpsUrl.annotate({ description: "URL of the MCP server
|
|
245
|
+
url: HttpsUrl.annotate({ description: "URL of the MCP server, called with `headers` if you set them, otherwise without credentials. To call it as a user, give a `connection_id` instead. Requests come from the addresses listed at `GET /egress-ips`." }),
|
|
246
|
+
headers: Schema.OptionFromOptionalKey(McpServerHeaders),
|
|
204
247
|
connection_id: Schema.optionalKey(Schema.Never)
|
|
205
248
|
}).annotate({
|
|
206
249
|
identifier: "McpServerByUrlInput",
|
|
@@ -208,13 +251,14 @@ const McpServerInput = Schema.Union([Schema.Struct({
|
|
|
208
251
|
}), Schema.Struct({
|
|
209
252
|
name: McpServerName,
|
|
210
253
|
connection_id: ConnectionId.annotate({ description: "An `active` connection: the agent uses its MCP server as the user who consented." }),
|
|
211
|
-
url: Schema.optionalKey(Schema.Never)
|
|
254
|
+
url: Schema.optionalKey(Schema.Never),
|
|
255
|
+
headers: Schema.optionalKey(Schema.Never)
|
|
212
256
|
}).annotate({
|
|
213
257
|
identifier: "McpServerByConnectionInput",
|
|
214
|
-
description: "An MCP server given by a connection to it,
|
|
258
|
+
description: "An MCP server given by a connection to it, called as the user who consented."
|
|
215
259
|
})]).annotate({
|
|
216
260
|
identifier: "McpServerInput",
|
|
217
|
-
description: "An MCP server the agent can use, reached through us: credentials never enter the sandbox. Set `url` or `connection_id`."
|
|
261
|
+
description: "An MCP server the agent can use, reached through us: credentials never enter the sandbox. Set `url`, with `headers` if it takes an API key, or `connection_id`."
|
|
218
262
|
});
|
|
219
263
|
const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
|
|
220
264
|
const names = servers.map((server) => server.name);
|
|
@@ -223,11 +267,66 @@ const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MA
|
|
|
223
267
|
path: [index, "name"],
|
|
224
268
|
issue: "Expected a name not reserved for the agent"
|
|
225
269
|
}] : []))).annotate({ description: `MCP servers the agent can use, at most ${MAX_MCP_SERVERS}.` });
|
|
270
|
+
const SKILL_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
271
|
+
const SKILL_ENTRY = "SKILL.md";
|
|
272
|
+
const MAX_SKILLS = 20;
|
|
273
|
+
const MAX_SKILL_FILES = 100;
|
|
274
|
+
const MAX_SKILLS_BYTES = 2097152;
|
|
275
|
+
const MAX_SKILL_DESCRIPTION_LENGTH = 1024;
|
|
276
|
+
const MAX_SKILL_PATH_LENGTH = 200;
|
|
277
|
+
const RESERVED_SKILL_NAMES = /* @__PURE__ */ new Set(["plugin-management"]);
|
|
278
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
279
|
+
const frontmatterValue = (content, key) => {
|
|
280
|
+
const block = FRONTMATTER.exec(content)?.[1];
|
|
281
|
+
const unquoted = (block === void 0 ? void 0 : new RegExp(`^${key}:[ \\t]*(.*)$`, "m").exec(block)?.[1])?.trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
282
|
+
return unquoted === "" ? void 0 : unquoted;
|
|
283
|
+
};
|
|
284
|
+
const SkillFileInput = Schema.Struct({
|
|
285
|
+
path: Schema.String.check(Schema.isMaxLength(MAX_SKILL_PATH_LENGTH), Schema.makeFilter((path) => !path.includes("\\") && path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..") || "Expected a plain relative path, such as scripts/build.sh")).annotate({ description: `Path of the file in the skill's folder, such as \`${SKILL_ENTRY}\` or \`scripts/build.sh\`, of up to ${MAX_SKILL_PATH_LENGTH} characters.` }),
|
|
286
|
+
content: Schema.String.annotate({ description: "Text of the file. A file starting with `#!` is made executable." })
|
|
287
|
+
}).annotate({
|
|
288
|
+
identifier: "SkillFileInput",
|
|
289
|
+
description: "A text file of a skill."
|
|
290
|
+
});
|
|
291
|
+
const skillEntryIssue = (skill) => {
|
|
292
|
+
const entry = skill.files.find((file) => file.path === SKILL_ENTRY);
|
|
293
|
+
if (entry === void 0) return `Expected a ${SKILL_ENTRY}`;
|
|
294
|
+
if (frontmatterValue(entry.content, "name") !== skill.name) return `Expected its ${SKILL_ENTRY} to give its name, ${skill.name}, in its frontmatter`;
|
|
295
|
+
const description = frontmatterValue(entry.content, "description");
|
|
296
|
+
if (description === void 0) return `Expected its ${SKILL_ENTRY} to give a description in its frontmatter`;
|
|
297
|
+
if (description.length > MAX_SKILL_DESCRIPTION_LENGTH) return `Expected a description of at most ${MAX_SKILL_DESCRIPTION_LENGTH} characters`;
|
|
298
|
+
return true;
|
|
299
|
+
};
|
|
300
|
+
const SkillInput = Schema.Struct({
|
|
301
|
+
name: Schema.String.check(Schema.isMaxLength(64), Schema.isPattern(SKILL_NAME)).annotate({ description: `Name of the skill, unique in the run, matching \`${SKILL_NAME.source}\` and up to 64 characters, such as \`release-notes\`. \`plugin-management\` is reserved for running the agent.` }),
|
|
302
|
+
files: Schema.Array(SkillFileInput).check(Schema.isMaxLength(MAX_SKILL_FILES), Schema.makeFilter((files) => {
|
|
303
|
+
const paths = files.map((file) => file.path);
|
|
304
|
+
return new Set(paths).size === paths.length || "Expected each file path once";
|
|
305
|
+
})).annotate({ description: `The files of the skill's folder, at most ${MAX_SKILL_FILES}, including a \`${SKILL_ENTRY}\` whose frontmatter gives the skill's \`name\` and a \`description\` of up to ${MAX_SKILL_DESCRIPTION_LENGTH} characters.` })
|
|
306
|
+
}).check(Schema.makeFilter((skill) => {
|
|
307
|
+
const issue = skillEntryIssue(skill);
|
|
308
|
+
return issue === true || {
|
|
309
|
+
path: ["files"],
|
|
310
|
+
issue
|
|
311
|
+
};
|
|
312
|
+
})).annotate({
|
|
313
|
+
identifier: "SkillInput",
|
|
314
|
+
description: "An Agent Skill: instructions and the text files they point to, installed for the agent before setup, outside the repository."
|
|
315
|
+
});
|
|
316
|
+
const encoder = new TextEncoder();
|
|
317
|
+
const SkillsInput = Schema.Array(SkillInput).check(Schema.isMaxLength(MAX_SKILLS), Schema.makeFilter((skills) => {
|
|
318
|
+
const names = skills.map((skill) => skill.name);
|
|
319
|
+
return new Set(names).size === names.length || "Skill names must be unique";
|
|
320
|
+
}), Schema.makeFilter((skills) => skills.flatMap((skill, index) => RESERVED_SKILL_NAMES.has(skill.name) ? [{
|
|
321
|
+
path: [index, "name"],
|
|
322
|
+
issue: "Expected a name not reserved for the agent"
|
|
323
|
+
}] : [])), Schema.makeFilter((skills) => skills.flatMap((skill) => skill.files).reduce((total, file) => total + encoder.encode(file.content).length, 0) <= MAX_SKILLS_BYTES || `Expected at most ${MAX_SKILLS_BYTES / 1024 / 1024} MB of skills together`)).annotate({ description: `Skills the agent can load, at most ${MAX_SKILLS} and ${MAX_SKILLS_BYTES / 1024 / 1024} MB together.` });
|
|
226
324
|
const EnvironmentInput = Schema.Struct({
|
|
227
325
|
repository,
|
|
228
326
|
variables: Schema.OptionFromOptionalKey(Variables$1),
|
|
229
327
|
setup,
|
|
230
328
|
teardown,
|
|
329
|
+
skills: Schema.OptionFromOptionalKey(SkillsInput),
|
|
231
330
|
mcp_servers: Schema.OptionFromOptionalKey(McpServersInput)
|
|
232
331
|
}).annotate({
|
|
233
332
|
identifier: "EnvironmentInput",
|
|
@@ -236,16 +335,25 @@ const EnvironmentInput = Schema.Struct({
|
|
|
236
335
|
const RunMcpServer = Schema.Struct({
|
|
237
336
|
name: Schema.String.annotate({ description: "Name the agent knows the MCP server by." }),
|
|
238
337
|
url: Schema.String.annotate({ description: "URL of the MCP server." }),
|
|
239
|
-
connection_id: Schema.OptionFromOptionalKey(ConnectionId.annotate({ description: "The connection the agent uses it through, if any." }))
|
|
338
|
+
connection_id: Schema.OptionFromOptionalKey(ConnectionId.annotate({ description: "The connection the agent uses it through, if any." })),
|
|
339
|
+
headers: Schema.Array(Schema.String).annotate({ description: "Names of the headers we add to every request to the MCP server. Their values are never returned." })
|
|
240
340
|
}).annotate({
|
|
241
341
|
identifier: "McpServer",
|
|
242
342
|
description: "An MCP server the agent can use."
|
|
243
343
|
});
|
|
344
|
+
const RunSkill = Schema.Struct({
|
|
345
|
+
name: Schema.String.annotate({ description: "Name of the skill." }),
|
|
346
|
+
files: Schema.Array(Schema.String).annotate({ description: "Paths of the files in the skill's folder." })
|
|
347
|
+
}).annotate({
|
|
348
|
+
identifier: "Skill",
|
|
349
|
+
description: "A skill the agent can load."
|
|
350
|
+
});
|
|
244
351
|
const RunEnvironment = Schema.Struct({
|
|
245
352
|
repository,
|
|
246
353
|
variables: Schema.Array(Schema.String).annotate({ description: "Names of the environment variables set in every process of the agent. Their values are never returned." }),
|
|
247
354
|
setup,
|
|
248
355
|
teardown,
|
|
356
|
+
skills: Schema.Array(RunSkill),
|
|
249
357
|
mcp_servers: Schema.Array(RunMcpServer)
|
|
250
358
|
}).annotate({
|
|
251
359
|
identifier: "Environment",
|
|
@@ -255,6 +363,7 @@ const LABEL_KEY_PATTERN = "[A-Za-z0-9_.-]{1,64}";
|
|
|
255
363
|
const LABEL_KEY = new RegExp(`^${LABEL_KEY_PATTERN}$`);
|
|
256
364
|
const MAX_LABELS = 20;
|
|
257
365
|
const MAX_LABEL_VALUE_LENGTH = 256;
|
|
366
|
+
const STARTED_BY_LABEL = "started_by";
|
|
258
367
|
const Labels = Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(MAX_LABEL_VALUE_LENGTH))).annotate({ description: `Labels you attach to the run to find it again, as keys and values, such as \`{ "ticket": "ENG-42" }\`. We never read them. Keys match \`${LABEL_KEY.source}\`; at most ${MAX_LABELS} labels with values of up to ${MAX_LABEL_VALUE_LENGTH} characters.` }).check(Schema.isMaxProperties(MAX_LABELS), Schema.makeFilter((labels) => Object.keys(labels).flatMap((key) => LABEL_KEY.test(key) ? [] : [{
|
|
259
368
|
path: [key],
|
|
260
369
|
issue: `Expected a key matching ${LABEL_KEY.source}`
|
|
@@ -271,17 +380,19 @@ const LabelFilters = Schema.Array(Schema.String.check(Schema.isPattern(LABEL_FIL
|
|
|
271
380
|
decode: (filters) => Object.fromEntries(filters.map(keyAndValueOf)),
|
|
272
381
|
encode: (labels) => Object.entries(labels).map(([key, value]) => `${key}:${value}`)
|
|
273
382
|
})));
|
|
383
|
+
const MAX_BUDGET_USD = 1e4;
|
|
274
384
|
const Budget = Schema.Finite.check(Schema.isBetween({
|
|
275
385
|
minimum: .01,
|
|
276
|
-
maximum:
|
|
386
|
+
maximum: MAX_BUDGET_USD
|
|
277
387
|
})).annotate({ description: "Maximum the run may spend, in US dollars. Defaults to a standard budget when omitted. The run ends with `budget_exceeded` once it is reached." });
|
|
278
388
|
const MIN_IDLE_TIMEOUT_SECONDS = 120;
|
|
279
389
|
const MAX_IDLE_TIMEOUT_SECONDS = 900;
|
|
280
390
|
const MIN_TIME_LIMIT_SECONDS = 60;
|
|
281
391
|
const MAX_TIME_LIMIT_SECONDS = 43200;
|
|
282
392
|
const CreateRunInput = Schema.Struct({
|
|
283
|
-
agent: Agent,
|
|
393
|
+
agent: Agent$1,
|
|
284
394
|
prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
|
|
395
|
+
input_files: Schema.optionalKey(Schema.Array(FileId).check(Schema.isMaxLength(20)).annotate({ description: "Up to 20 uploaded file ids, staged in the sandbox before setup and referenced in the first prompt." })),
|
|
285
396
|
environment: EnvironmentInput,
|
|
286
397
|
labels: Schema.optionalKey(Labels),
|
|
287
398
|
budget_usd: Schema.OptionFromOptionalKey(Budget),
|
|
@@ -294,7 +405,10 @@ const CreateRunInput = Schema.Struct({
|
|
|
294
405
|
minimum: MIN_TIME_LIMIT_SECONDS,
|
|
295
406
|
maximum: MAX_TIME_LIMIT_SECONDS
|
|
296
407
|
})).annotate({ description: `Longest the run may last, in seconds from when it starts, from ${MIN_TIME_LIMIT_SECONDS} to ${MAX_TIME_LIMIT_SECONDS}. Defaults to 7200. Once reached, the run ends with \`time_limit\`, even in the middle of a prompt.` }))
|
|
297
|
-
}).check(Schema.makeFilter((input) => Option.contains(input.one_shot, false) || Option.isNone(input.idle_timeout_seconds) || "idle_timeout_seconds is only allowed when one_shot is false")).
|
|
408
|
+
}).check(Schema.makeFilter((input) => Option.contains(input.one_shot, false) || Option.isNone(input.idle_timeout_seconds) || "idle_timeout_seconds is only allowed when one_shot is false"), Schema.makeFilter((input) => Object.keys(input.labels ?? {}).filter((key) => key !== STARTED_BY_LABEL).length < MAX_LABELS || {
|
|
409
|
+
path: ["labels"],
|
|
410
|
+
issue: `Expected at most 19 labels besides ${STARTED_BY_LABEL}, which is set to the API key`
|
|
411
|
+
})).annotate({ identifier: "CreateRunInput" });
|
|
298
412
|
const SendPromptInput = Schema.Struct({
|
|
299
413
|
text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
|
|
300
414
|
model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
|
|
@@ -350,8 +464,9 @@ const FailureKind = Schema.Literals([
|
|
|
350
464
|
});
|
|
351
465
|
const runFields = {
|
|
352
466
|
id: RunId,
|
|
467
|
+
input_files: Schema.Array(InputFile),
|
|
353
468
|
labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
|
|
354
|
-
agent: Agent,
|
|
469
|
+
agent: Agent$1,
|
|
355
470
|
environment: RunEnvironment,
|
|
356
471
|
budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
|
|
357
472
|
one_shot: Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes." }),
|
|
@@ -406,7 +521,7 @@ var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: S
|
|
|
406
521
|
}) {};
|
|
407
522
|
var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
|
|
408
523
|
httpApiStatus: 422,
|
|
409
|
-
description: "A `connection_id` in `environment.mcp_servers` does not exist or is not `active
|
|
524
|
+
description: "A `connection_id` in `environment.mcp_servers` does not exist or is not `active`."
|
|
410
525
|
}) {};
|
|
411
526
|
var ModelNotOffered = class extends Schema.TaggedError()("ModelNotOffered", { message: Schema.String }, {
|
|
412
527
|
httpApiStatus: 422,
|
|
@@ -431,13 +546,13 @@ var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Sch
|
|
|
431
546
|
httpApiStatus: 409,
|
|
432
547
|
description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
|
|
433
548
|
}) {};
|
|
434
|
-
const Count = Schema.Natural;
|
|
549
|
+
const Count$1 = Schema.Natural;
|
|
435
550
|
const RunUsage = Schema.Struct({
|
|
436
551
|
cost_usd: Schema.Number.annotate({ description: "Total cost of the run so far, in US dollars." }),
|
|
437
552
|
models: Schema.Array(Schema.Struct({
|
|
438
553
|
provider: Schema.String.annotate({ description: "Company serving the model, such as `anthropic`." }),
|
|
439
554
|
model: Schema.String,
|
|
440
|
-
quantities: Schema.Record(Schema.String, Count).annotate({ description: "Units consumed, keyed by unit as the provider counts them, such as input tokens or web searches." }),
|
|
555
|
+
quantities: Schema.Record(Schema.String, Count$1).annotate({ description: "Units consumed, keyed by unit as the provider counts them, such as input tokens or web searches." }),
|
|
441
556
|
cost_usd: Schema.Number.annotate({ description: "Cost of this model's usage, in US dollars." })
|
|
442
557
|
})).annotate({ description: "Model usage, one entry per model." }),
|
|
443
558
|
sandbox: Schema.Struct({
|
|
@@ -566,19 +681,15 @@ const EventStreamItem = Schema.Union([Schema.Struct({
|
|
|
566
681
|
event: Schema.Literal("heartbeat"),
|
|
567
682
|
data: Schema.Literal("")
|
|
568
683
|
})]);
|
|
569
|
-
const STARTED_BY_LABEL = "started_by";
|
|
570
|
-
const CreateRunInputBesidesStartedBy = CreateRunInput.check(Schema.makeFilter((input) => Object.keys(input.labels ?? {}).filter((key) => key !== STARTED_BY_LABEL).length < MAX_LABELS || {
|
|
571
|
-
path: ["labels"],
|
|
572
|
-
issue: `Expected at most 19 labels besides ${STARTED_BY_LABEL}, which is set to the API key`
|
|
573
|
-
}));
|
|
574
684
|
var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
|
|
575
685
|
headers: { "idempotency-key": Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(255)).annotate({ description: "A unique value you choose, up to 255 characters, to retry safely: a request with a key already used in your organization returns the run the first request created, without starting another, whatever its body." })) },
|
|
576
|
-
payload:
|
|
686
|
+
payload: CreateRunInput,
|
|
577
687
|
success: Run.pipe(HttpApiSchema.status(201)),
|
|
578
688
|
error: [
|
|
579
689
|
ModelNotOffered,
|
|
580
690
|
ModelProviderUnavailable,
|
|
581
691
|
ConnectionUnusable,
|
|
692
|
+
FileUnavailable,
|
|
582
693
|
InsufficientBalance,
|
|
583
694
|
OrganizationBlocked
|
|
584
695
|
]
|
|
@@ -686,8 +797,328 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
686
797
|
title: "Runs",
|
|
687
798
|
description: "Start coding agents and follow their work."
|
|
688
799
|
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
689
|
-
|
|
690
|
-
|
|
800
|
+
// ../contract/src/experiments.ts
|
|
801
|
+
const ExperimentId = Schema.String.pipe(Schema.brand("ExperimentId")).annotate({
|
|
802
|
+
identifier: "ExperimentId",
|
|
803
|
+
description: "Unique identifier of the experiment."
|
|
804
|
+
});
|
|
805
|
+
const ExecutionId = Schema.String.pipe(Schema.brand("ExecutionId")).annotate({
|
|
806
|
+
identifier: "ExecutionId",
|
|
807
|
+
description: "Unique identifier of the execution."
|
|
808
|
+
});
|
|
809
|
+
const Agent = Schema.Struct({
|
|
810
|
+
harness: Harness,
|
|
811
|
+
model: Schema.String.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`." })
|
|
812
|
+
}).annotate({
|
|
813
|
+
identifier: "ExperimentAgent",
|
|
814
|
+
description: "A coding agent and the model it uses."
|
|
815
|
+
});
|
|
816
|
+
const MAX_ID_LENGTH = 128;
|
|
817
|
+
const idChosenFor = (item, example) => Schema.NonEmptyString.check(Schema.isMaxLength(MAX_ID_LENGTH)).annotate({ description: `Identifier you choose for the ${item}, up to ${MAX_ID_LENGTH} characters, such as \`${example}\`. Results refer to it.` });
|
|
818
|
+
const withUniqueIds = (item, items) => Schema.Array(item).check(Schema.makeFilter((all) => {
|
|
819
|
+
const ids = all.map((each) => each.id);
|
|
820
|
+
return new Set(ids).size === ids.length || `Expected each of the ${items}' ids once`;
|
|
821
|
+
}));
|
|
822
|
+
const libraryIds = (items) => Schema.Array(Schema.String).annotate({ description: `Ids of ${items} from your library, as listed by \`GET /library\`.` });
|
|
823
|
+
const Evaluation = Schema.Struct({
|
|
824
|
+
id: idChosenFor("evaluation, unique in its scenario", "tests-pass"),
|
|
825
|
+
name: Schema.String.annotate({ description: "Short name of what is checked." }),
|
|
826
|
+
judging_instructions: Schema.String.annotate({ description: "The yes-or-no question a judge answers from what the agent did, such as `Do the tests pass?`." }),
|
|
827
|
+
weight: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "How much it counts in the run's score. Defaults to 1." })
|
|
828
|
+
}).annotate({
|
|
829
|
+
identifier: "Evaluation",
|
|
830
|
+
description: "One criterion every run of the scenario is judged on."
|
|
831
|
+
});
|
|
832
|
+
const MAX_INPUT_FILES = 20;
|
|
833
|
+
const Scenario = Schema.Struct({
|
|
834
|
+
id: idChosenFor("scenario, unique in the experiment", "checkout"),
|
|
835
|
+
name: Schema.String.annotate({ description: "Short name of the scenario." }),
|
|
836
|
+
prompt: Schema.String.annotate({ description: "What the agent is asked to do." }),
|
|
837
|
+
input_files: Schema.Array(FileId).check(Schema.isMaxLength(MAX_INPUT_FILES)).annotate({ description: `Up to ${MAX_INPUT_FILES} uploaded file ids, given to each of the scenario's runs. Kept for good once saved. Defaults to none.` }),
|
|
838
|
+
evaluations: withUniqueIds(Evaluation, "evaluations").annotate({ description: "What a run is judged on. Runs of a scenario without evaluations are not judged." }),
|
|
839
|
+
passing_score: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
840
|
+
minimum: 0,
|
|
841
|
+
maximum: 100
|
|
842
|
+
})).annotate({ description: "Score out of 100, weighted by the evaluations' weights, a run needs to pass. Omit it so a run passes only when it passes every evaluation." }))
|
|
843
|
+
}).annotate({
|
|
844
|
+
identifier: "Scenario",
|
|
845
|
+
description: "One task given to the agents."
|
|
846
|
+
});
|
|
847
|
+
const AgentTools = Schema.Struct({
|
|
848
|
+
id: idChosenFor("agent tool configuration, unique in the experiment", "with-docs-mcp"),
|
|
849
|
+
name: Schema.String.annotate({ description: "Short name of the agent tools, such as `No agent tools` for the baseline." }),
|
|
850
|
+
skills: libraryIds("skills"),
|
|
851
|
+
mcp_servers: libraryIds("MCP servers"),
|
|
852
|
+
command_line_tools: libraryIds("command-line tools")
|
|
853
|
+
}).annotate({
|
|
854
|
+
identifier: "AgentTools",
|
|
855
|
+
description: "Skills, MCP servers and command-line tools given to the agent. Keep one with none as the baseline the others are compared to."
|
|
856
|
+
});
|
|
857
|
+
const ExcludedVariant = Schema.Struct({
|
|
858
|
+
repository_id: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Id of one of the experiment's repositories. Omit it for no repository." })),
|
|
859
|
+
agent_tools_id: Schema.String.annotate({ description: "Id of one of the experiment's agent tool configurations." })
|
|
860
|
+
}).annotate({
|
|
861
|
+
identifier: "ExcludedVariant",
|
|
862
|
+
description: "A combination of repository and agent tools not to run."
|
|
863
|
+
});
|
|
864
|
+
const RunSettings = Schema.Struct({
|
|
865
|
+
repetitions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Identical runs started for each scenario, variant and agent, to compare results beyond chance." }),
|
|
866
|
+
time_limit_seconds: Schema.Finite.check(Schema.isBetween({
|
|
867
|
+
minimum: 0,
|
|
868
|
+
exclusiveMinimum: true,
|
|
869
|
+
maximum: MAX_TIME_LIMIT_SECONDS
|
|
870
|
+
})).annotate({ description: `Longest each run may last, in seconds, above 0 and at most ${MAX_TIME_LIMIT_SECONDS}.` }),
|
|
871
|
+
budget_usd: Schema.Finite.check(Schema.isBetween({
|
|
872
|
+
minimum: 0,
|
|
873
|
+
exclusiveMinimum: true,
|
|
874
|
+
maximum: MAX_BUDGET_USD
|
|
875
|
+
})).annotate({ description: `Most each run may spend, in US dollars, above 0 and at most ${MAX_BUDGET_USD}.` }),
|
|
876
|
+
setup: script("Shell script run in each run's sandbox after checkout and before the prompt."),
|
|
877
|
+
teardown: script("Shell script run in each run's sandbox after the run ends."),
|
|
878
|
+
variables: Schema.Array(Schema.Struct({
|
|
879
|
+
name: Schema.String,
|
|
880
|
+
value: Schema.String
|
|
881
|
+
})).annotate({ description: "Environment variables set in every process of each run's agent, each name once. Defaults to none." })
|
|
882
|
+
}).annotate({
|
|
883
|
+
identifier: "RunSettings",
|
|
884
|
+
description: "What every run of the experiment shares."
|
|
885
|
+
});
|
|
886
|
+
const experimentFields = {
|
|
887
|
+
name: Schema.String.annotate({ description: "Name of the experiment." }),
|
|
888
|
+
scenarios: withUniqueIds(Scenario, "scenarios"),
|
|
889
|
+
agents: Schema.Array(Agent).annotate({ description: "Agents every scenario is given to, each once." }),
|
|
890
|
+
repositories: libraryIds("repositories").annotate({ description: "Ids of repositories from your library, as listed by `GET /library`, each once, tried in turn with every agent tool configuration. Defaults to none: runs start without a repository." }),
|
|
891
|
+
agent_tools: withUniqueIds(AgentTools, "agent_tools").annotate({ description: "Agent tool configurations each tried in turn with every repository." }),
|
|
892
|
+
excluded_variants: Schema.Array(ExcludedVariant).annotate({ description: "Combinations of repository and agent tools not to run. Every other combination is a variant, run for each scenario and agent. Defaults to none." }),
|
|
893
|
+
run_settings: RunSettings
|
|
894
|
+
};
|
|
895
|
+
const Experiment = Schema.Struct({
|
|
896
|
+
id: ExperimentId,
|
|
897
|
+
...experimentFields
|
|
898
|
+
}).annotate({
|
|
899
|
+
identifier: "Experiment",
|
|
900
|
+
description: "Scenarios given to several agents in several variants, each run judged on the scenario's evaluations."
|
|
901
|
+
});
|
|
902
|
+
const EvaluationInput = Evaluation.mapFields(Struct.evolve({ weight: (weight) => weight.pipe(Schema.withDecodingDefaultKey(Effect.succeed(1))) })).annotate({ identifier: "EvaluationInput" });
|
|
903
|
+
const ScenarioInput = Scenario.mapFields(Struct.evolve({
|
|
904
|
+
input_files: (inputFiles) => inputFiles.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
905
|
+
evaluations: () => withUniqueIds(EvaluationInput, "evaluations")
|
|
906
|
+
})).annotate({ identifier: "ScenarioInput" });
|
|
907
|
+
const RunSettingsInput = RunSettings.mapFields(Struct.evolve({ variables: (variables) => variables.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))) })).annotate({ identifier: "RunSettingsInput" });
|
|
908
|
+
const unknownExclusions = (input) => input.excluded_variants.flatMap((excluded, index) => [...input.agent_tools.some((agentTools) => agentTools.id === excluded.agent_tools_id) ? [] : [{
|
|
909
|
+
path: [
|
|
910
|
+
"excluded_variants",
|
|
911
|
+
index,
|
|
912
|
+
"agent_tools_id"
|
|
913
|
+
],
|
|
914
|
+
issue: "Expected the id of one of the agent tool configurations"
|
|
915
|
+
}], ...Option.match(excluded.repository_id, {
|
|
916
|
+
onNone: () => true,
|
|
917
|
+
onSome: (repository) => input.repositories.includes(repository)
|
|
918
|
+
}) ? [] : [{
|
|
919
|
+
path: [
|
|
920
|
+
"excluded_variants",
|
|
921
|
+
index,
|
|
922
|
+
"repository_id"
|
|
923
|
+
],
|
|
924
|
+
issue: "Expected one of the repositories"
|
|
925
|
+
}]]);
|
|
926
|
+
const ExperimentInput = Schema.Struct(experimentFields).mapFields(Struct.evolve({
|
|
927
|
+
scenarios: () => withUniqueIds(ScenarioInput, "scenarios"),
|
|
928
|
+
repositories: (repositories) => repositories.check(Schema.isUnique()).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
929
|
+
excluded_variants: (excluded) => excluded.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
930
|
+
run_settings: () => RunSettingsInput
|
|
931
|
+
})).check(Schema.makeFilter(unknownExclusions)).annotate({ identifier: "ExperimentInput" });
|
|
932
|
+
const Count = Schema.Natural;
|
|
933
|
+
const Execution = Schema.Struct({
|
|
934
|
+
id: ExecutionId,
|
|
935
|
+
experiment_id: ExperimentId,
|
|
936
|
+
number: Schema.Int.annotate({ description: "Its place among the experiment's executions, from 1." }),
|
|
937
|
+
launched_at: Schema.DateTimeUtc.annotate({ description: "When the execution was launched." }),
|
|
938
|
+
canceled_at: Schema.OptionFromOptionalKey(Schema.DateTimeUtc.annotate({ description: "When the execution was canceled. Absent unless it was." })),
|
|
939
|
+
pending: Schema.Boolean.annotate({ description: "Whether some of its runs have not ended yet or wait for their evaluation results. Its pass rate is final once it is `false`, canceled or not." }),
|
|
940
|
+
slots: Schema.Struct({
|
|
941
|
+
total: Count.annotate({ description: "Runs the execution plans." }),
|
|
942
|
+
running: Count.annotate({ description: "Runs not started yet or not ended." }),
|
|
943
|
+
judging: Count.annotate({ description: "Ended runs waiting for their evaluation results." }),
|
|
944
|
+
judged: Count.annotate({ description: "Runs with a verdict." }),
|
|
945
|
+
passed: Count.annotate({ description: "Runs that passed their scenario." })
|
|
946
|
+
}).annotate({ description: "How many of its planned runs stand where." }),
|
|
947
|
+
pass_rate: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
948
|
+
minimum: 0,
|
|
949
|
+
maximum: 100
|
|
950
|
+
})).annotate({ description: "Runs that passed out of those with a verdict, as a whole percent. Absent until one run has a verdict." })),
|
|
951
|
+
cost_usd: Schema.Number.annotate({ description: "What its runs cost so far, in US dollars." })
|
|
952
|
+
}).annotate({
|
|
953
|
+
identifier: "Execution",
|
|
954
|
+
description: "One launch of an experiment: a run for every scenario, variant, agent and repetition, with the experiment as it was when launched."
|
|
955
|
+
});
|
|
956
|
+
const NotEvaluatedReason = Schema.Literals([
|
|
957
|
+
"never_started",
|
|
958
|
+
"no_evaluations",
|
|
959
|
+
"run_not_completed",
|
|
960
|
+
"agent_failed",
|
|
961
|
+
"evidence_too_long",
|
|
962
|
+
"too_many_attempts"
|
|
963
|
+
]).annotate({ description: "Why the run has no verdict: it never started, its scenario has no evaluations, it did not end `completed`, its agent stopped with an error such as its model provider being unreachable, what it did was too long to judge, or judging failed too many times." });
|
|
964
|
+
const Slot = Schema.Struct({
|
|
965
|
+
id: Schema.String.annotate({ description: "Unique identifier of the slot." }),
|
|
966
|
+
scenario_id: Schema.String,
|
|
967
|
+
repository_id: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Absent when the run has no repository." })),
|
|
968
|
+
agent_tools_id: Schema.String,
|
|
969
|
+
agent: Agent,
|
|
970
|
+
repetition: Schema.Int.annotate({ description: "Which of the identical runs it is, from 1." }),
|
|
971
|
+
run_id: Schema.OptionFromOptionalKey(RunId.annotate({ description: "The run, once started." })),
|
|
972
|
+
outcome: Schema.Literals([
|
|
973
|
+
"running",
|
|
974
|
+
"judging",
|
|
975
|
+
"judged",
|
|
976
|
+
"not_evaluated"
|
|
977
|
+
]).annotate({ description: "`running` while its run waits to start or has not ended, `judging` while it waits for evaluation results, `judged` once they are all in, or `not_evaluated` when it never will be." }),
|
|
978
|
+
verdict: Schema.OptionFromOptionalKey(Schema.Literals(["passed", "failed"]).annotate({ description: "Whether the run passed its scenario, once judged." })),
|
|
979
|
+
score: Schema.OptionFromOptionalKey(Schema.Number.annotate({ description: "Weighted share of evaluations passed, out of 100, once every evaluation result is in." })),
|
|
980
|
+
not_evaluated_reason: Schema.OptionFromOptionalKey(NotEvaluatedReason),
|
|
981
|
+
refusal: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Why its run could not start." })),
|
|
982
|
+
evaluation_results: Schema.Array(Schema.Struct({
|
|
983
|
+
evaluation_id: Schema.String,
|
|
984
|
+
passed: Schema.Boolean
|
|
985
|
+
})).annotate({ description: "Each evaluation judged so far, and whether the run passed it." })
|
|
986
|
+
}).annotate({
|
|
987
|
+
identifier: "Slot",
|
|
988
|
+
description: "One run an execution plans, for one scenario, variant, agent and repetition, and how it was judged."
|
|
989
|
+
});
|
|
990
|
+
var ExperimentNotFound = class extends Schema.TaggedError()("ExperimentNotFound", { message: Schema.String }, {
|
|
991
|
+
httpApiStatus: 404,
|
|
992
|
+
description: "No experiment with this id exists in your organization."
|
|
993
|
+
}) {};
|
|
994
|
+
var ExecutionNotFound = class extends Schema.TaggedError()("ExecutionNotFound", { message: Schema.String }, {
|
|
995
|
+
httpApiStatus: 404,
|
|
996
|
+
description: "No execution with this id exists in your organization."
|
|
997
|
+
}) {};
|
|
998
|
+
var ExperimentIncomplete = class extends Schema.TaggedError()("ExperimentIncomplete", {
|
|
999
|
+
message: Schema.String,
|
|
1000
|
+
missing: Schema.Array(Schema.String).annotate({ description: "Each thing to fix before launching, in a sentence." })
|
|
1001
|
+
}, {
|
|
1002
|
+
httpApiStatus: 422,
|
|
1003
|
+
description: "The experiment cannot be launched as it is: `missing` says what to fix."
|
|
1004
|
+
}) {};
|
|
1005
|
+
const experimentParams = { id: ExperimentId };
|
|
1006
|
+
var ExperimentsApi = class extends HttpApiGroup.make("experiments").add(HttpApiEndpoint.post("create", "/experiments", {
|
|
1007
|
+
payload: ExperimentInput,
|
|
1008
|
+
success: Experiment.pipe(HttpApiSchema.status(201)),
|
|
1009
|
+
error: FileUnavailable
|
|
1010
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1011
|
+
summary: "Create an experiment",
|
|
1012
|
+
description: "Creates an experiment from its scenarios, agents, variants and run settings. Launch it to start its runs."
|
|
1013
|
+
})), HttpApiEndpoint.get("list", "/experiments", { success: List(Experiment) }).annotateMerge(OpenApi.annotations({
|
|
1014
|
+
summary: "List experiments",
|
|
1015
|
+
description: "Returns every experiment."
|
|
1016
|
+
})), HttpApiEndpoint.get("get", "/experiments/:id", {
|
|
1017
|
+
params: experimentParams,
|
|
1018
|
+
success: Experiment,
|
|
1019
|
+
error: ExperimentNotFound
|
|
1020
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1021
|
+
summary: "Get an experiment",
|
|
1022
|
+
description: "Returns an experiment."
|
|
1023
|
+
})), HttpApiEndpoint.put("update", "/experiments/:id", {
|
|
1024
|
+
params: experimentParams,
|
|
1025
|
+
payload: ExperimentInput,
|
|
1026
|
+
success: Experiment,
|
|
1027
|
+
error: [ExperimentNotFound, FileUnavailable]
|
|
1028
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1029
|
+
summary: "Update an experiment",
|
|
1030
|
+
description: "Replaces the whole experiment: send it complete, as `GET /experiments/{id}` returns it. Executions already launched keep the experiment as it was."
|
|
1031
|
+
})), HttpApiEndpoint.delete("delete", "/experiments/:id", {
|
|
1032
|
+
params: experimentParams,
|
|
1033
|
+
success: HttpApiSchema.NoContent,
|
|
1034
|
+
error: ExperimentNotFound
|
|
1035
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1036
|
+
summary: "Delete an experiment",
|
|
1037
|
+
description: "Deletes an experiment. Its executions, their runs and results stay."
|
|
1038
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1039
|
+
title: "Experiments",
|
|
1040
|
+
description: "Define scenarios to give several agents in several variants, judged on what they did."
|
|
1041
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1042
|
+
const executionParams = { id: ExecutionId };
|
|
1043
|
+
var ExecutionsApi = class extends HttpApiGroup.make("executions").add(HttpApiEndpoint.post("launch", "/experiments/:id/executions", {
|
|
1044
|
+
params: experimentParams,
|
|
1045
|
+
success: Execution.pipe(HttpApiSchema.status(201)),
|
|
1046
|
+
error: [
|
|
1047
|
+
ExperimentNotFound,
|
|
1048
|
+
ExperimentIncomplete,
|
|
1049
|
+
InsufficientBalance,
|
|
1050
|
+
OrganizationBlocked
|
|
1051
|
+
]
|
|
1052
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1053
|
+
summary: "Launch an experiment",
|
|
1054
|
+
description: "Starts an execution of the experiment as it is now: a run for every scenario, variant, agent and repetition, at most 500, started in the background. Each run is judged on its scenario's evaluations once it ends. Executions start only while your organization's balance is above $0."
|
|
1055
|
+
})), HttpApiEndpoint.get("list", "/experiments/:id/executions", {
|
|
1056
|
+
params: experimentParams,
|
|
1057
|
+
success: List(Execution)
|
|
1058
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1059
|
+
summary: "List an experiment's executions",
|
|
1060
|
+
description: "Returns the experiment's executions, the latest launched first, even after it was deleted."
|
|
1061
|
+
})), HttpApiEndpoint.get("get", "/executions/:id", {
|
|
1062
|
+
params: executionParams,
|
|
1063
|
+
success: Execution,
|
|
1064
|
+
error: ExecutionNotFound
|
|
1065
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1066
|
+
summary: "Get an execution",
|
|
1067
|
+
description: "Returns an execution, where its runs stand and its pass rate. Poll it until it is no longer `pending`."
|
|
1068
|
+
})), HttpApiEndpoint.get("listSlots", "/executions/:id/slots", {
|
|
1069
|
+
params: executionParams,
|
|
1070
|
+
success: List(Slot),
|
|
1071
|
+
error: ExecutionNotFound
|
|
1072
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1073
|
+
summary: "List an execution's slots",
|
|
1074
|
+
description: "Returns every run the execution plans, with its run once started and its verdict once judged. Read a run's work with the runs endpoints."
|
|
1075
|
+
})), HttpApiEndpoint.post("cancel", "/executions/:id/cancel", {
|
|
1076
|
+
params: executionParams,
|
|
1077
|
+
success: Execution.pipe(HttpApiSchema.status(202)),
|
|
1078
|
+
error: ExecutionNotFound
|
|
1079
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1080
|
+
summary: "Cancel an execution",
|
|
1081
|
+
description: "Starts none of its runs left and cancels those started. Canceling an execution already canceled does nothing."
|
|
1082
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1083
|
+
title: "Executions",
|
|
1084
|
+
description: "Launch experiments and read their results."
|
|
1085
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1086
|
+
// ../contract/src/library.ts
|
|
1087
|
+
const id = Schema.String.annotate({ description: "Id to pick it in an experiment." });
|
|
1088
|
+
const Library = Schema.Struct({
|
|
1089
|
+
repositories: Schema.Array(Schema.Struct({
|
|
1090
|
+
id,
|
|
1091
|
+
name: Schema.String,
|
|
1092
|
+
url: Schema.String.annotate({ description: "URL of the Git repository." }),
|
|
1093
|
+
ref: Schema.String.annotate({ description: "Branch, tag or commit checked out." })
|
|
1094
|
+
})),
|
|
1095
|
+
skills: Schema.Array(Schema.Struct({
|
|
1096
|
+
id,
|
|
1097
|
+
name: Schema.String,
|
|
1098
|
+
description: Schema.String
|
|
1099
|
+
})),
|
|
1100
|
+
mcp_servers: Schema.Array(Schema.Struct({
|
|
1101
|
+
id,
|
|
1102
|
+
name: Schema.String.annotate({ description: "Name the agent knows the MCP server by." }),
|
|
1103
|
+
url: Schema.String,
|
|
1104
|
+
connected: Schema.Boolean.annotate({ description: "Whether runs can use it: always for one without an account, otherwise once connected in the web app." })
|
|
1105
|
+
})),
|
|
1106
|
+
command_line_tools: Schema.Array(Schema.Struct({
|
|
1107
|
+
id,
|
|
1108
|
+
name: Schema.String
|
|
1109
|
+
}))
|
|
1110
|
+
}).annotate({
|
|
1111
|
+
identifier: "Library",
|
|
1112
|
+
description: "What your organization keeps for its experiments, added in the web app."
|
|
1113
|
+
});
|
|
1114
|
+
var LibraryApi = class extends HttpApiGroup.make("library").add(HttpApiEndpoint.get("get", "/library", { success: Library }).annotateMerge(OpenApi.annotations({
|
|
1115
|
+
summary: "Get the library",
|
|
1116
|
+
description: "Returns the repositories, skills, MCP servers and command-line tools your experiments can pick, by id."
|
|
1117
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1118
|
+
title: "Library",
|
|
1119
|
+
description: "What your experiments can give their agents."
|
|
1120
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1121
|
+
// ../contract/src/system.ts
|
|
691
1122
|
const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
|
|
692
1123
|
var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
|
|
693
1124
|
summary: "Check health",
|
|
@@ -696,14 +1127,12 @@ var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).ad
|
|
|
696
1127
|
summary: "List egress IPs",
|
|
697
1128
|
description: "The fixed addresses we call MCP servers and their authorization servers from. Allow them on an MCP server that only accepts known addresses. Commands the agent runs in its sandbox, such as cloning the repository, come from other addresses. No key required."
|
|
698
1129
|
}))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(ConnectionsApi).add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
|
|
1130
|
+
// ../contract/src/api.ts
|
|
1131
|
+
var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(FilesApi).add(ConnectionsApi).add(ExperimentsApi).add(ExecutionsApi).add(LibraryApi).add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
|
|
702
1132
|
title: "Tracks API",
|
|
703
|
-
description: "Run coding agents such as Claude Code in isolated sandboxes with an API key of your organization. Create a run with a prompt, then follow its output and what it costs."
|
|
1133
|
+
description: "Run coding agents such as Claude Code in isolated sandboxes with an API key of your organization. Create a run with a prompt, then follow its output and what it costs, or launch an experiment to compare agents on your scenarios."
|
|
704
1134
|
})) {};
|
|
705
|
-
|
|
706
|
-
//#region ../contract/src/client.ts
|
|
1135
|
+
// ../contract/src/client.ts
|
|
707
1136
|
const CLI_USER_AGENT = "tracks";
|
|
708
1137
|
var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
|
|
709
1138
|
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
|
|
@@ -711,8 +1140,7 @@ var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiC
|
|
|
711
1140
|
return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
|
|
712
1141
|
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
|
|
713
1142
|
};
|
|
714
|
-
|
|
715
|
-
//#region ../env/src/env.ts
|
|
1143
|
+
// ../env/src/env.ts
|
|
716
1144
|
const Secret = Schema.Redacted(Schema.NonEmptyString);
|
|
717
1145
|
const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
|
|
718
1146
|
const Port = Schema.Int.check(Schema.isBetween({
|
|
@@ -749,8 +1177,12 @@ const Secrets = Schema.Struct({
|
|
|
749
1177
|
TYPESAFE_API_KEY: Secret,
|
|
750
1178
|
DATABASE_PASSWORD: Secret,
|
|
751
1179
|
CONNECTION_ENCRYPTION_KEY: EncryptionKey,
|
|
1180
|
+
TRAFFIC_ENCRYPTION_KEY: EncryptionKey,
|
|
752
1181
|
STRIPE_SECRET_KEY: StripeKey("rk_"),
|
|
753
1182
|
STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
|
|
1183
|
+
IDENTITY_PROVIDER_CLIENT_ID: Secret,
|
|
1184
|
+
IDENTITY_PROVIDER_CLIENT_SECRET: Secret,
|
|
1185
|
+
SESSION_SECRET: Secret,
|
|
754
1186
|
SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
|
|
755
1187
|
});
|
|
756
1188
|
const Variables = Schema.Struct({
|
|
@@ -758,6 +1190,8 @@ const Variables = Schema.Struct({
|
|
|
758
1190
|
TRACKS_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${TRACKS_API_PATH}`))),
|
|
759
1191
|
DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
|
|
760
1192
|
AGENT_RUNNER_URL: SecureUrl,
|
|
1193
|
+
FILES_BUCKET: Schema.NonEmptyString,
|
|
1194
|
+
FILES_ENDPOINT: Schema.optionalKey(SecureUrl),
|
|
761
1195
|
CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
|
|
762
1196
|
EGRESS_IPS: IpAddresses.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
|
|
763
1197
|
DATABASE_HOST: Schema.NonEmptyString,
|
|
@@ -776,7 +1210,11 @@ const Variables = Schema.Struct({
|
|
|
776
1210
|
]).pipe(Schema.withDecodingDefaultKey(Effect.succeed("Info"))),
|
|
777
1211
|
DEPLOYMENT_ENVIRONMENT: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("local"))),
|
|
778
1212
|
OTLP_METRICS_URL: Schema.optionalKey(Schema.URLFromString),
|
|
779
|
-
|
|
1213
|
+
RELEASE: Schema.optionalKey(Schema.NonEmptyString),
|
|
1214
|
+
POSTHOG_KEY: Schema.optionalKey(Schema.NonEmptyString),
|
|
1215
|
+
POSTHOG_HOST: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://us.i.posthog.com"))),
|
|
1216
|
+
CLOUDFLARE_CLIENT_ID: Schema.NonEmptyString,
|
|
1217
|
+
IDENTITY_PROVIDER_URL: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://idp.alpic.ai")))
|
|
780
1218
|
});
|
|
781
1219
|
const Env = Schema.Struct({
|
|
782
1220
|
...Secrets.fields,
|
|
@@ -806,7 +1244,16 @@ const CONNECTIONS_ENV = [
|
|
|
806
1244
|
"CONNECTION_ENCRYPTION_KEY"
|
|
807
1245
|
];
|
|
808
1246
|
const PAYMENTS_ENV = ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"];
|
|
1247
|
+
const IDENTITY_ENV = [
|
|
1248
|
+
"IDENTITY_PROVIDER_URL",
|
|
1249
|
+
"IDENTITY_PROVIDER_CLIENT_ID",
|
|
1250
|
+
"IDENTITY_PROVIDER_CLIENT_SECRET",
|
|
1251
|
+
"SESSION_SECRET"
|
|
1252
|
+
];
|
|
1253
|
+
const TRAFFIC_ENV = ["CLOUDFLARE_CLIENT_ID", "TRAFFIC_ENCRYPTION_KEY"];
|
|
809
1254
|
const AGENT_RUNNER_ENV = [
|
|
1255
|
+
"FILES_BUCKET",
|
|
1256
|
+
"FILES_ENDPOINT",
|
|
810
1257
|
...DATABASE_ENV,
|
|
811
1258
|
...DAYTONA_ENV,
|
|
812
1259
|
...E2B_ENV,
|
|
@@ -816,27 +1263,30 @@ const AGENT_RUNNER_ENV = [
|
|
|
816
1263
|
const LOG_ENV = ["LOG_FORMAT", "LOG_LEVEL"];
|
|
817
1264
|
const OBSERVABILITY_ENV = [
|
|
818
1265
|
"SENTRY_DSN",
|
|
1266
|
+
"POSTHOG_KEY",
|
|
1267
|
+
"POSTHOG_HOST",
|
|
819
1268
|
"DEPLOYMENT_ENVIRONMENT",
|
|
820
|
-
"OTLP_METRICS_URL"
|
|
1269
|
+
"OTLP_METRICS_URL",
|
|
1270
|
+
"RELEASE"
|
|
821
1271
|
];
|
|
822
1272
|
const SERVICE_ENV = [
|
|
1273
|
+
"TYPESAFE_API_KEY",
|
|
823
1274
|
"PORT",
|
|
824
1275
|
"EGRESS_IPS",
|
|
825
|
-
"LOAD_BALANCER_ARN",
|
|
826
1276
|
...LOG_ENV,
|
|
827
1277
|
...OBSERVABILITY_ENV,
|
|
828
1278
|
...AGENT_RUNNER_ENV,
|
|
829
|
-
...PAYMENTS_ENV
|
|
1279
|
+
...PAYMENTS_ENV,
|
|
1280
|
+
...IDENTITY_ENV,
|
|
1281
|
+
...TRAFFIC_ENV
|
|
830
1282
|
];
|
|
831
1283
|
const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
|
|
832
1284
|
const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
|
|
833
1285
|
SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
|
|
834
1286
|
const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
//#endregion
|
|
839
|
-
//#region src/connections.ts
|
|
1287
|
+
// package.json
|
|
1288
|
+
var version = "0.7.1";
|
|
1289
|
+
// src/connections.ts
|
|
840
1290
|
const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
|
|
841
1291
|
const encodeConnections = Schema.encodeEffect(Schema.fromJsonString(List(Connection), { space: 2 }));
|
|
842
1292
|
const connectionId = Argument.String("id").pipe(Argument.withDescription("Connection id"), Argument.withSchema(ConnectionId));
|
|
@@ -899,7 +1349,7 @@ const confirmationOf = (params, id) => {
|
|
|
899
1349
|
onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
|
|
900
1350
|
});
|
|
901
1351
|
};
|
|
902
|
-
const create$
|
|
1352
|
+
const create$2 = Command.make("create", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.create")(function* ({ server }) {
|
|
903
1353
|
const client = yield* ApiClient;
|
|
904
1354
|
const callback = yield* loopbackCallback;
|
|
905
1355
|
const created = yield* client.connections.create({ payload: {
|
|
@@ -919,11 +1369,11 @@ const create$1 = Command.make("create", { server: Argument.String("server-url").
|
|
|
919
1369
|
});
|
|
920
1370
|
yield* Console.log(yield* encodeConnection(connection));
|
|
921
1371
|
}, Effect.scoped)).pipe(Command.withDescription("Connect to an MCP server as one of its users: consent in your browser, then use the connection in runs"));
|
|
922
|
-
const list = Command.make("list", {}, Effect.fn("connections.list")(function* () {
|
|
1372
|
+
const list$1 = Command.make("list", {}, Effect.fn("connections.list")(function* () {
|
|
923
1373
|
const client = yield* ApiClient;
|
|
924
1374
|
yield* Console.log(yield* encodeConnections(yield* client.connections.list()));
|
|
925
1375
|
})).pipe(Command.withDescription("List connections"));
|
|
926
|
-
const get$
|
|
1376
|
+
const get$2 = Command.make("get", { id: connectionId }, Effect.fn("connections.get")(function* ({ id }) {
|
|
927
1377
|
const client = yield* ApiClient;
|
|
928
1378
|
yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
|
|
929
1379
|
})).pipe(Command.withDescription("Show a connection"));
|
|
@@ -931,13 +1381,143 @@ const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn(
|
|
|
931
1381
|
yield* (yield* ApiClient).connections.delete({ params: { id } });
|
|
932
1382
|
})).pipe(Command.withDescription("Delete a connection and the access it holds"));
|
|
933
1383
|
const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
|
|
1384
|
+
create$2,
|
|
1385
|
+
list$1,
|
|
1386
|
+
get$2,
|
|
1387
|
+
deleteConnection
|
|
1388
|
+
]));
|
|
1389
|
+
// src/experiments.ts
|
|
1390
|
+
const encodeExperiment = Schema.encodeEffect(Schema.fromJsonString(Experiment, { space: 2 }));
|
|
1391
|
+
const encodeExperiments = Schema.encodeEffect(Schema.fromJsonString(List(Experiment), { space: 2 }));
|
|
1392
|
+
const encodeExecution = Schema.encodeEffect(Schema.fromJsonString(Execution, { space: 2 }));
|
|
1393
|
+
const encodeExecutions = Schema.encodeEffect(Schema.fromJsonString(List(Execution), { space: 2 }));
|
|
1394
|
+
const encodeSlots = Schema.encodeEffect(Schema.fromJsonString(List(Slot), { space: 2 }));
|
|
1395
|
+
const experimentId = Argument.String("id").pipe(Argument.withDescription("Experiment id"), Argument.withSchema(ExperimentId));
|
|
1396
|
+
const executionId = Argument.String("id").pipe(Argument.withDescription("Execution id"), Argument.withSchema(ExecutionId));
|
|
1397
|
+
const definition = Argument.FileSchema("file", ExperimentInput).pipe(Argument.withDescription("JSON file of the experiment, as `tracks experiments get` prints it, without its id"));
|
|
1398
|
+
const POLL_INTERVAL = "10 seconds";
|
|
1399
|
+
var PassRateTooLow = class extends Schema.TaggedError()("PassRateTooLow", {
|
|
1400
|
+
passRate: Schema.Option(Schema.Number),
|
|
1401
|
+
minimum: Schema.Number
|
|
1402
|
+
}) {
|
|
1403
|
+
get message() {
|
|
1404
|
+
const reached = Option.match(this.passRate, {
|
|
1405
|
+
onNone: () => "no run was judged",
|
|
1406
|
+
onSome: (rate) => `${rate}%`
|
|
1407
|
+
});
|
|
1408
|
+
return `The execution's pass rate is below ${this.minimum}%: ${reached}`;
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
const create$1 = Command.make("create", { definition }, Effect.fn("experiments.create")(function* ({ definition: payload }) {
|
|
1412
|
+
const client = yield* ApiClient;
|
|
1413
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.create({ payload })));
|
|
1414
|
+
})).pipe(Command.withDescription("Create an experiment from a JSON file"));
|
|
1415
|
+
const list = Command.make("list", {}, Effect.fn("experiments.list")(function* () {
|
|
1416
|
+
const client = yield* ApiClient;
|
|
1417
|
+
yield* Console.log(yield* encodeExperiments(yield* client.experiments.list()));
|
|
1418
|
+
})).pipe(Command.withDescription("List experiments"));
|
|
1419
|
+
const get$1 = Command.make("get", { id: experimentId }, Effect.fn("experiments.get")(function* ({ id }) {
|
|
1420
|
+
const client = yield* ApiClient;
|
|
1421
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.get({ params: { id } })));
|
|
1422
|
+
})).pipe(Command.withDescription("Show an experiment"));
|
|
1423
|
+
const update = Command.make("update", {
|
|
1424
|
+
id: experimentId,
|
|
1425
|
+
definition
|
|
1426
|
+
}, Effect.fn("experiments.update")(function* ({ id, definition: payload }) {
|
|
1427
|
+
const client = yield* ApiClient;
|
|
1428
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.update({
|
|
1429
|
+
params: { id },
|
|
1430
|
+
payload
|
|
1431
|
+
})));
|
|
1432
|
+
})).pipe(Command.withDescription("Replace a whole experiment with a JSON file"));
|
|
1433
|
+
const deleteExperiment = Command.make("delete", { id: experimentId }, Effect.fn("experiments.delete")(function* ({ id }) {
|
|
1434
|
+
yield* (yield* ApiClient).experiments.delete({ params: { id } });
|
|
1435
|
+
})).pipe(Command.withDescription("Delete an experiment; its executions and their results stay"));
|
|
1436
|
+
const launch = Command.make("launch", {
|
|
1437
|
+
id: experimentId,
|
|
1438
|
+
wait: Flag.Boolean("wait").pipe(Flag.withAlias("w"), Flag.withDescription(`Wait until no run is pending, checking every ${POLL_INTERVAL}, then print the execution`), Flag.withDefault(false)),
|
|
1439
|
+
minPassRate: Flag.Int("min-pass-rate").pipe(Flag.withSchema(Schema.Int.check(Schema.isBetween({
|
|
1440
|
+
minimum: 0,
|
|
1441
|
+
maximum: 100
|
|
1442
|
+
}))), Flag.withDescription("Wait, then exit with an error when the pass rate ends below this percentage, from 0 to 100"), Flag.optional)
|
|
1443
|
+
}, Effect.fn("experiments.launch")(function* ({ id, wait, minPassRate }) {
|
|
1444
|
+
const client = yield* ApiClient;
|
|
1445
|
+
const launched = yield* client.executions.launch({ params: { id } });
|
|
1446
|
+
if (!wait && Option.isNone(minPassRate)) return yield* Console.log(yield* encodeExecution(launched));
|
|
1447
|
+
yield* Console.error(`Launched execution ${launched.id}: ${launched.slots.total} runs`);
|
|
1448
|
+
const done = yield* client.executions.get({ params: { id: launched.id } }).pipe(Effect.tap(({ slots }) => Console.error(`${slots.running} running, ${slots.judging} judging, ${slots.judged} judged`)), Effect.repeat({
|
|
1449
|
+
schedule: Schedule.spaced(POLL_INTERVAL),
|
|
1450
|
+
until: (execution) => !execution.pending
|
|
1451
|
+
}));
|
|
1452
|
+
yield* Console.log(yield* encodeExecution(done));
|
|
1453
|
+
if (Option.isSome(minPassRate) && !Option.exists(done.pass_rate, (rate) => rate >= minPassRate.value)) return yield* new PassRateTooLow({
|
|
1454
|
+
passRate: done.pass_rate,
|
|
1455
|
+
minimum: minPassRate.value
|
|
1456
|
+
});
|
|
1457
|
+
})).pipe(Command.withDescription("Launch an experiment: start a run for every scenario, variant, agent and repetition"));
|
|
1458
|
+
const executionsOfExperiment = Command.make("executions", { id: experimentId }, Effect.fn("experiments.executions")(function* ({ id }) {
|
|
1459
|
+
const client = yield* ApiClient;
|
|
1460
|
+
yield* Console.log(yield* encodeExecutions(yield* client.executions.list({ params: { id } })));
|
|
1461
|
+
})).pipe(Command.withDescription("List an experiment's executions"));
|
|
1462
|
+
const experiments = Command.make("experiments").pipe(Command.withDescription("Compare agents on your scenarios"), Command.withSubcommands([
|
|
934
1463
|
create$1,
|
|
935
1464
|
list,
|
|
936
1465
|
get$1,
|
|
937
|
-
|
|
1466
|
+
update,
|
|
1467
|
+
deleteExperiment,
|
|
1468
|
+
launch,
|
|
1469
|
+
executionsOfExperiment
|
|
938
1470
|
]));
|
|
939
|
-
|
|
940
|
-
|
|
1471
|
+
const getExecution = Command.make("get", { id: executionId }, Effect.fn("executions.get")(function* ({ id }) {
|
|
1472
|
+
const client = yield* ApiClient;
|
|
1473
|
+
yield* Console.log(yield* encodeExecution(yield* client.executions.get({ params: { id } })));
|
|
1474
|
+
})).pipe(Command.withDescription("Show an execution, where its runs stand and its pass rate"));
|
|
1475
|
+
const slots = Command.make("slots", { id: executionId }, Effect.fn("executions.slots")(function* ({ id }) {
|
|
1476
|
+
const client = yield* ApiClient;
|
|
1477
|
+
yield* Console.log(yield* encodeSlots(yield* client.executions.listSlots({ params: { id } })));
|
|
1478
|
+
})).pipe(Command.withDescription("List every run an execution plans, with its verdict once judged"));
|
|
1479
|
+
const cancel$1 = Command.make("cancel", { id: executionId }, Effect.fn("executions.cancel")(function* ({ id }) {
|
|
1480
|
+
const client = yield* ApiClient;
|
|
1481
|
+
yield* Console.log(yield* encodeExecution(yield* client.executions.cancel({ params: { id } })));
|
|
1482
|
+
})).pipe(Command.withDescription("Cancel an execution: start none of its runs left and cancel those started"));
|
|
1483
|
+
const executions = Command.make("executions").pipe(Command.withDescription("Follow and cancel experiments' executions"), Command.withSubcommands([
|
|
1484
|
+
getExecution,
|
|
1485
|
+
slots,
|
|
1486
|
+
cancel$1
|
|
1487
|
+
]));
|
|
1488
|
+
// src/library.ts
|
|
1489
|
+
const encodeLibrary = Schema.encodeEffect(Schema.fromJsonString(Library, { space: 2 }));
|
|
1490
|
+
const library = Command.make("library", {}, Effect.fn("library")(function* () {
|
|
1491
|
+
const client = yield* ApiClient;
|
|
1492
|
+
yield* Console.log(yield* encodeLibrary(yield* client.library.get()));
|
|
1493
|
+
})).pipe(Command.withDescription("List the repositories, skills, MCP servers and command-line tools experiments can pick"));
|
|
1494
|
+
// src/files.ts
|
|
1495
|
+
const CONTENT_TYPES = {
|
|
1496
|
+
".png": "image/png",
|
|
1497
|
+
".jpg": "image/jpeg",
|
|
1498
|
+
".jpeg": "image/jpeg",
|
|
1499
|
+
".webp": "image/webp",
|
|
1500
|
+
".gif": "image/gif",
|
|
1501
|
+
".pdf": "application/pdf",
|
|
1502
|
+
".txt": "text/plain",
|
|
1503
|
+
".md": "text/markdown",
|
|
1504
|
+
".csv": "text/csv",
|
|
1505
|
+
".json": "application/json"
|
|
1506
|
+
};
|
|
1507
|
+
const uploadFile = Effect.fn("uploadFile")(function* (localPath) {
|
|
1508
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
1509
|
+
const path = yield* Path.Path;
|
|
1510
|
+
const contentType = CONTENT_TYPES[path.extname(localPath).toLowerCase()] ?? "application/octet-stream";
|
|
1511
|
+
const bytes = yield* fileSystem.readFile(localPath);
|
|
1512
|
+
const upload = yield* (yield* ApiClient).files.createUpload({ payload: {
|
|
1513
|
+
name: path.basename(localPath),
|
|
1514
|
+
content_type: contentType,
|
|
1515
|
+
size: bytes.byteLength
|
|
1516
|
+
} });
|
|
1517
|
+
yield* (yield* HttpClient.HttpClient).execute(HttpClientRequest.put(upload.url).pipe(HttpClientRequest.bodyUint8Array(bytes, contentType), HttpClientRequest.setHeaders(upload.headers))).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk));
|
|
1518
|
+
return upload.id;
|
|
1519
|
+
});
|
|
1520
|
+
// src/runs.ts
|
|
941
1521
|
const encodeRun = Schema.encodeEffect(Schema.fromJsonString(Run, { space: 2 }));
|
|
942
1522
|
const encodeFrames = Schema.encodeEffect(Schema.fromJsonString(List(Frame), { space: 2 }));
|
|
943
1523
|
const encodeEvents = Schema.encodeEffect(Schema.fromJsonString(List(Event), { space: 2 }));
|
|
@@ -965,27 +1545,64 @@ const mcpServerOf = (entry) => {
|
|
|
965
1545
|
const target = entry.slice(equals + 1);
|
|
966
1546
|
return Option.some(URL.canParse(target) ? {
|
|
967
1547
|
name,
|
|
968
|
-
url: target
|
|
1548
|
+
url: target,
|
|
1549
|
+
headers: Option.none()
|
|
969
1550
|
} : {
|
|
970
1551
|
name,
|
|
971
1552
|
connection_id: ConnectionId.make(target)
|
|
972
1553
|
});
|
|
973
1554
|
};
|
|
1555
|
+
const fixedHeaderOf = (entry) => {
|
|
1556
|
+
const equals = entry.indexOf("=");
|
|
1557
|
+
const colon = entry.indexOf(":", equals);
|
|
1558
|
+
if (equals === -1 || colon === -1) return Option.none();
|
|
1559
|
+
return Option.some({
|
|
1560
|
+
server: entry.slice(0, equals),
|
|
1561
|
+
name: entry.slice(equals + 1, colon).trim(),
|
|
1562
|
+
value: entry.slice(colon + 1).trim()
|
|
1563
|
+
});
|
|
1564
|
+
};
|
|
1565
|
+
var FixedHeaderUnmatched = class extends Schema.TaggedError()("FixedHeaderUnmatched", { server: Schema.String }) {
|
|
1566
|
+
get message() {
|
|
1567
|
+
return `--mcp-header ${this.server}=… needs an --mcp ${this.server}=URL`;
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
974
1570
|
const create = Command.make("create", {
|
|
975
1571
|
prompt: Argument.String("prompt").pipe(Argument.withDescription("Prompt sent to the agent")),
|
|
976
1572
|
harness: Flag.Literals("harness", Harness.literals).pipe(Flag.withAlias("H"), Flag.withDescription("Harness running the agent")),
|
|
977
1573
|
model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model used by the harness")),
|
|
1574
|
+
file: Flag.File("file", { mustExist: true }).pipe(Flag.withAlias("f"), Flag.withDescription("Local file uploaded and placed in the sandbox before setup, its path given to the agent with the prompt; repeat for several"), Flag.atLeast(0)),
|
|
978
1575
|
repository: Flag.String("repository").pipe(Flag.withAlias("r"), Flag.withDescription("Git repository cloned into the sandbox"), Flag.optional),
|
|
979
1576
|
env: Flag.String("env").pipe(Flag.withAlias("e"), Flag.withDescription("Environment variable set in the agent's processes, as KEY=VALUE, or KEY to pass its local value"), Flag.filterMap(variableOf, (entry) => `${entry} is not set in the local environment`), Flag.atLeast(0)),
|
|
980
1577
|
setup: Flag.String("setup").pipe(Flag.withDescription("Shell script run in the sandbox after checkout, before the first prompt; from a file: --setup \"$(cat setup.sh)\""), Flag.optional),
|
|
981
1578
|
teardown: Flag.String("teardown").pipe(Flag.withDescription("Shell script run in the sandbox after the run ends, before the sandbox is destroyed; from a file: --teardown \"$(cat teardown.sh)\""), Flag.optional),
|
|
982
|
-
mcp: Flag.String("mcp").pipe(Flag.withDescription("MCP server the agent can use, as NAME=URL,
|
|
1579
|
+
mcp: Flag.String("mcp").pipe(Flag.withDescription("MCP server the agent can use, as NAME=URL, called without credentials, or NAME=CONNECTION_ID, called as the user who connected it"), Flag.filterMap(mcpServerOf, (entry) => `${entry} is not NAME=URL or NAME=CONNECTION_ID`), Flag.atLeast(0)),
|
|
1580
|
+
mcpHeader: Flag.String("mcp-header").pipe(Flag.withDescription("Header added to every request to the MCP server NAME given by URL, typically its API key, as NAME=HEADER: VALUE, such as 'docs=Authorization: Bearer KEY'"), Flag.mapEffect((entry) => Option.match(fixedHeaderOf(entry), {
|
|
1581
|
+
onNone: () => Effect.fail(new CliError.InvalidValue({
|
|
1582
|
+
option: "mcp-header",
|
|
1583
|
+
value: "(hidden, it may hold a key)",
|
|
1584
|
+
expected: "NAME=HEADER: VALUE",
|
|
1585
|
+
kind: "flag"
|
|
1586
|
+
})),
|
|
1587
|
+
onSome: Effect.succeed
|
|
1588
|
+
})), Flag.atLeast(0)),
|
|
983
1589
|
budget: Flag.Finite("budget").pipe(Flag.withAlias("b"), Flag.withDescription("Most the run may spend, in US dollars"), Flag.optional),
|
|
984
1590
|
oneShot: Flag.Boolean("one-shot").pipe(Flag.withDescription("End the run once its first prompt finishes (default); --no-one-shot waits for follow-ups"), Flag.optional),
|
|
985
1591
|
idleTimeout: Flag.Int("idle-timeout").pipe(Flag.withDescription("Seconds an idle run waits for a follow-up before it ends"), Flag.optional),
|
|
986
1592
|
timeLimit: Flag.Int("time-limit").pipe(Flag.withDescription("Longest the run may last, in seconds, before it is stopped"), Flag.optional),
|
|
987
1593
|
idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional)
|
|
988
|
-
}, Effect.fn("runs.create")(function* ({ prompt, harness, model, repository, env, setup, teardown, mcp, budget, oneShot, idleTimeout, timeLimit, idempotencyKey }) {
|
|
1594
|
+
}, Effect.fn("runs.create")(function* ({ prompt, harness, model, file, repository, env, setup, teardown, mcp, mcpHeader, budget, oneShot, idleTimeout, timeLimit, idempotencyKey }) {
|
|
1595
|
+
const unmatched = mcpHeader.find((header) => !mcp.some((server) => server.url !== void 0 && server.name === header.server));
|
|
1596
|
+
if (unmatched !== void 0) return yield* new FixedHeaderUnmatched({ server: unmatched.server });
|
|
1597
|
+
const mcpServers = mcp.map((server) => {
|
|
1598
|
+
const headers = mcpHeader.filter((header) => header.server === server.name);
|
|
1599
|
+
if (server.url === void 0 || headers.length === 0) return server;
|
|
1600
|
+
return {
|
|
1601
|
+
...server,
|
|
1602
|
+
headers: Option.some(Object.fromEntries(headers.map(({ name, value }) => [name, value])))
|
|
1603
|
+
};
|
|
1604
|
+
});
|
|
1605
|
+
const inputFiles = yield* Effect.forEach(file, uploadFile, { concurrency: "unbounded" });
|
|
989
1606
|
const run = yield* (yield* ApiClient).runs.create({
|
|
990
1607
|
headers: { "idempotency-key": idempotencyKey },
|
|
991
1608
|
payload: {
|
|
@@ -994,12 +1611,14 @@ const create = Command.make("create", {
|
|
|
994
1611
|
model
|
|
995
1612
|
},
|
|
996
1613
|
prompt,
|
|
1614
|
+
input_files: inputFiles,
|
|
997
1615
|
environment: {
|
|
998
1616
|
repository,
|
|
999
1617
|
variables: Option.some(Object.fromEntries(env.map(({ name, value }) => [name, value]))),
|
|
1000
1618
|
setup,
|
|
1001
1619
|
teardown,
|
|
1002
|
-
|
|
1620
|
+
skills: Option.none(),
|
|
1621
|
+
mcp_servers: Option.some(mcpServers)
|
|
1003
1622
|
},
|
|
1004
1623
|
budget_usd: budget,
|
|
1005
1624
|
one_shot: oneShot,
|
|
@@ -1117,27 +1736,28 @@ const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), C
|
|
|
1117
1736
|
end,
|
|
1118
1737
|
cancel
|
|
1119
1738
|
]));
|
|
1120
|
-
|
|
1121
|
-
//#region src/main.ts
|
|
1739
|
+
// src/main.ts
|
|
1122
1740
|
const health = Command.make("health", {}, Effect.fn("health")(function* () {
|
|
1123
1741
|
yield* (yield* ApiClient).health();
|
|
1124
1742
|
yield* Console.log("ok");
|
|
1125
1743
|
})).pipe(Command.withDescription("Check that the API is reachable"));
|
|
1126
|
-
const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes"), Command.withSubcommands([
|
|
1744
|
+
const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes and compare them on your scenarios"), Command.withSubcommands([
|
|
1127
1745
|
health,
|
|
1128
1746
|
runs,
|
|
1129
|
-
connections
|
|
1747
|
+
connections,
|
|
1748
|
+
experiments,
|
|
1749
|
+
executions,
|
|
1750
|
+
library
|
|
1130
1751
|
]));
|
|
1131
1752
|
const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
|
|
1132
1753
|
baseUrl: env.TRACKS_API_URL,
|
|
1133
1754
|
apiKey: env.TRACKS_API_KEY,
|
|
1134
1755
|
userAgent: `${CLI_USER_AGENT}/${version}`
|
|
1135
|
-
})))
|
|
1136
|
-
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
|
|
1756
|
+
})));
|
|
1757
|
+
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer).pipe(Layer.provideMerge(FetchHttpClient.layer));
|
|
1137
1758
|
Command.run(cli, { version }).pipe(Effect.catch((error) => {
|
|
1138
1759
|
const explainedByHelp = error._tag === "ShowHelp";
|
|
1139
1760
|
const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
|
|
1140
1761
|
return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = 1)));
|
|
1141
1762
|
}), Effect.provide(MainLayer), NodeRuntime.runMain);
|
|
1142
|
-
//#endregion
|
|
1143
1763
|
export {};
|