@alpic-ai/tracks 0.7.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +154 -38
- package/package.json +5 -2
package/dist/main.js
CHANGED
|
@@ -181,7 +181,10 @@ var FilesApi = class extends HttpApiGroup.make("files").add(HttpApiEndpoint.post
|
|
|
181
181
|
}).annotateMerge(OpenApi.annotations({
|
|
182
182
|
summary: "Request a file upload",
|
|
183
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
|
-
}))).
|
|
184
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
185
|
+
title: "Files",
|
|
186
|
+
description: "Upload files to place in a run's sandbox."
|
|
187
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
185
188
|
// ../contract/src/runs.ts
|
|
186
189
|
const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
|
|
187
190
|
identifier: "RunId",
|
|
@@ -321,14 +324,23 @@ const SkillsInput = Schema.Array(SkillInput).check(Schema.isMaxLength(MAX_SKILLS
|
|
|
321
324
|
path: [index, "name"],
|
|
322
325
|
issue: "Expected a name not reserved for the agent"
|
|
323
326
|
}] : [])), 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.` });
|
|
327
|
+
const BROWSER_MCP_SERVER = "tracks-browser";
|
|
324
328
|
const EnvironmentInput = Schema.Struct({
|
|
325
329
|
repository,
|
|
326
330
|
variables: Schema.OptionFromOptionalKey(Variables$1),
|
|
327
331
|
setup,
|
|
328
332
|
teardown,
|
|
329
333
|
skills: Schema.OptionFromOptionalKey(SkillsInput),
|
|
330
|
-
mcp_servers: Schema.OptionFromOptionalKey(McpServersInput)
|
|
331
|
-
|
|
334
|
+
mcp_servers: Schema.OptionFromOptionalKey(McpServersInput),
|
|
335
|
+
browser: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))).annotate({ description: "Whether the agent gets a browser: a headless Chromium in its sandbox, driven through an MCP server named `tracks-browser`, whose pages can offer it tools through WebMCP. Screenshots of the pages it visits are kept with the run. Ignored by the Codex app and Claude Cowork, which always use their own browser. Defaults to false." })
|
|
336
|
+
}).check(Schema.makeFilter((environment) => !environment.browser ? [] : Option.getOrElse(environment.mcp_servers, () => []).flatMap((server, index) => server.name === BROWSER_MCP_SERVER ? [{
|
|
337
|
+
path: [
|
|
338
|
+
"mcp_servers",
|
|
339
|
+
index,
|
|
340
|
+
"name"
|
|
341
|
+
],
|
|
342
|
+
issue: `Expected a name other than ${BROWSER_MCP_SERVER}, the browser's MCP server`
|
|
343
|
+
}] : []))).annotate({
|
|
332
344
|
identifier: "EnvironmentInput",
|
|
333
345
|
description: "What the agent works on."
|
|
334
346
|
});
|
|
@@ -354,7 +366,8 @@ const RunEnvironment = Schema.Struct({
|
|
|
354
366
|
setup,
|
|
355
367
|
teardown,
|
|
356
368
|
skills: Schema.Array(RunSkill),
|
|
357
|
-
mcp_servers: Schema.Array(RunMcpServer)
|
|
369
|
+
mcp_servers: Schema.Array(RunMcpServer),
|
|
370
|
+
browser: Schema.Boolean.annotate({ description: "Whether the agent was given a headless Chromium in its sandbox." })
|
|
358
371
|
}).annotate({
|
|
359
372
|
identifier: "Environment",
|
|
360
373
|
description: "What the agent works on."
|
|
@@ -454,13 +467,15 @@ const EndReason = Schema.Literals([
|
|
|
454
467
|
const FailureKind = Schema.Literals([
|
|
455
468
|
"repository_checkout_failed",
|
|
456
469
|
"setup_failed",
|
|
470
|
+
"browser_not_started",
|
|
457
471
|
"sandbox_not_found",
|
|
458
472
|
"process_lost",
|
|
459
473
|
"sandbox_unavailable",
|
|
474
|
+
"agent_stopped",
|
|
460
475
|
"unexpected"
|
|
461
476
|
]).annotate({
|
|
462
477
|
identifier: "FailureKind",
|
|
463
|
-
description: "What kind of thing went wrong: the repository could not be checked out, the environment's setup command failed, the sandbox disappeared, the agent's process was lost, the sandbox provider failed, or something unexpected."
|
|
478
|
+
description: "What kind of thing went wrong: the repository could not be checked out, the environment's setup command failed, the browser did not start, the sandbox disappeared, the agent's process was lost, the sandbox provider failed, the agent of a run that accepts no follow-ups stopped with an error, or something unexpected."
|
|
464
479
|
});
|
|
465
480
|
const runFields = {
|
|
466
481
|
id: RunId,
|
|
@@ -519,6 +534,10 @@ var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: S
|
|
|
519
534
|
httpApiStatus: 404,
|
|
520
535
|
description: "No run with this id exists in your organization."
|
|
521
536
|
}) {};
|
|
537
|
+
var ScreenshotNotFound = class extends Schema.TaggedError()("ScreenshotNotFound", { message: Schema.String }, {
|
|
538
|
+
httpApiStatus: 404,
|
|
539
|
+
description: "This event has no screenshot."
|
|
540
|
+
}) {};
|
|
522
541
|
var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
|
|
523
542
|
httpApiStatus: 422,
|
|
524
543
|
description: "A `connection_id` in `environment.mcp_servers` does not exist or is not `active`."
|
|
@@ -567,18 +586,23 @@ const Frame = Schema.Struct({
|
|
|
567
586
|
run_id: RunId,
|
|
568
587
|
seq: Schema.Int.annotate({ description: "Position of the line in the run's output, starting at 1. Always increasing, but some numbers may be skipped." }),
|
|
569
588
|
prompt: PromptIndex.annotate({ description: "Index of the prompt the agent was working on." }),
|
|
589
|
+
process: Schema.String.annotate({ description: "Program in the sandbox that printed the line: `prompt-1`, `prompt-2`… for the agent working on each prompt, `browser` for its browser." }),
|
|
570
590
|
captured_at: Schema.DateTimeUtc.annotate({ description: "When the line was captured." }),
|
|
571
591
|
channel: Schema.Literals(["stdout", "stderr"]).annotate({ description: "Output stream the line came from." }),
|
|
572
592
|
body: Schema.String.annotate({ description: "The line, exactly as the agent printed it." })
|
|
573
593
|
}).annotate({
|
|
574
594
|
identifier: "Frame",
|
|
575
|
-
description: "One line of the agent
|
|
595
|
+
description: "One line of output from the agent or its browser."
|
|
576
596
|
});
|
|
577
597
|
const Outcome = Schema.Literals(["succeeded", "failed"]).annotate({
|
|
578
598
|
identifier: "Outcome",
|
|
579
599
|
description: "Whether the tool call or subagent succeeded."
|
|
580
600
|
});
|
|
581
601
|
const ToolCallId = Schema.String.annotate({ description: "Identifier of the tool call, unique in its run." });
|
|
602
|
+
const Screenshot = Schema.Struct({ page_url: Schema.String.annotate({ description: "Address of the page shown." }) }).annotate({
|
|
603
|
+
identifier: "Screenshot",
|
|
604
|
+
description: "An image of the page the agent's browser showed; get it from `GET /runs/{id}/events/{seq}/screenshot`, with this event's `seq`."
|
|
605
|
+
});
|
|
582
606
|
const eventFields = {
|
|
583
607
|
run_id: RunId,
|
|
584
608
|
seq: Schema.Int.annotate({ description: "Position of the event in the run, starting at 1. Always increasing, but some numbers may be skipped." }),
|
|
@@ -628,7 +652,8 @@ const Event = Schema.Union([
|
|
|
628
652
|
tool_call_id: ToolCallId,
|
|
629
653
|
outcome: Outcome,
|
|
630
654
|
output: Schema.String.annotate({ description: "What the tool returned, possibly truncated." }),
|
|
631
|
-
structured: Schema.OptionFromOptionalKey(Schema.Json.annotate({ description: "The structured content an MCP tool returned beside its text, unchanged. Absent when the tool returned none or it was too large." }))
|
|
655
|
+
structured: Schema.OptionFromOptionalKey(Schema.Json.annotate({ description: "The structured content an MCP tool returned beside its text, unchanged. Absent when the tool returned none or it was too large." })),
|
|
656
|
+
screenshot: Schema.OptionFromOptionalKey(Screenshot.annotate({ description: "The page a browser tool call left its browser on, as the harness captured it. Absent when it captured none." }))
|
|
632
657
|
}).annotate({
|
|
633
658
|
identifier: "ToolEndedEvent",
|
|
634
659
|
description: "A tool call ended; its `tool_call_id` is the one of the `tool_started` event."
|
|
@@ -649,6 +674,27 @@ const Event = Schema.Union([
|
|
|
649
674
|
identifier: "SubagentEndedEvent",
|
|
650
675
|
description: "A subagent ended."
|
|
651
676
|
}),
|
|
677
|
+
Schema.Struct({
|
|
678
|
+
...eventFields,
|
|
679
|
+
type: Schema.Literal("page_visited"),
|
|
680
|
+
tool_call_id: Schema.OptionFromOptionalKey(ToolCallId.annotate({ description: "The browser tool call that led to the page, when the agent had made one." })),
|
|
681
|
+
page_id: Schema.String.annotate({ description: "Identifier of the page in the run: a later visit to the same page keeps it." }),
|
|
682
|
+
version: Schema.Int.annotate({ description: "How many times the page was seen changed, this one included, starting at 1." }),
|
|
683
|
+
url: Schema.String.annotate({ description: "URL of the page." }),
|
|
684
|
+
title: Schema.String.annotate({ description: "Title of the page, empty when it has none." }),
|
|
685
|
+
reason: Schema.Literals([
|
|
686
|
+
"arrival",
|
|
687
|
+
"settled",
|
|
688
|
+
"after_submit",
|
|
689
|
+
"error",
|
|
690
|
+
"after_action",
|
|
691
|
+
"prompt_end"
|
|
692
|
+
]).annotate({ description: "Why it was captured: on arrival, once settled, after a form was submitted, on an error, after a browser action, or when the prompt ended." }),
|
|
693
|
+
screenshot: Schema.OptionFromOptionalKey(Screenshot.annotate({ description: "The page as the browser captured it. Absent when it captured none; its image may be missing when the sandbox was gone before it was kept." }))
|
|
694
|
+
}).annotate({
|
|
695
|
+
identifier: "PageVisitedEvent",
|
|
696
|
+
description: "The agent's browser showed a page, new or changed."
|
|
697
|
+
}),
|
|
652
698
|
Schema.Struct({
|
|
653
699
|
...eventFields,
|
|
654
700
|
type: Schema.Literal("stop"),
|
|
@@ -777,6 +823,16 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
777
823
|
}).annotateMerge(OpenApi.annotations({
|
|
778
824
|
summary: "List a run's events",
|
|
779
825
|
description: "Returns what the agent did, in order: each prompt, then its messages, reasoning, tool calls and subagents, then a `stop` once the agent stopped. Events read alike whatever the harness, and are read from the run's output: `frame_seqs` points at the lines each one comes from. To get the next page, pass the `seq` of the last event received as `after`."
|
|
826
|
+
})), HttpApiEndpoint.get("getScreenshot", "/runs/:id/events/:seq/screenshot", {
|
|
827
|
+
params: {
|
|
828
|
+
id: RunId,
|
|
829
|
+
seq: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
|
|
830
|
+
},
|
|
831
|
+
success: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array({ contentType: "image/jpeg" })),
|
|
832
|
+
error: [RunNotFound, ScreenshotNotFound]
|
|
833
|
+
}).annotateMerge(OpenApi.annotations({
|
|
834
|
+
summary: "Get a screenshot",
|
|
835
|
+
description: "Returns the image of a page the agent's browser showed, for a `tool_ended` or `page_visited` event that has a `screenshot`. Its content type is the image's own, usually `image/jpeg`."
|
|
780
836
|
})), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
|
|
781
837
|
params: { id: RunId },
|
|
782
838
|
query: { after: OptionalQueryParam(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), "Send only lines whose `seq` is greater than this.") },
|
|
@@ -849,10 +905,11 @@ const AgentTools = Schema.Struct({
|
|
|
849
905
|
name: Schema.String.annotate({ description: "Short name of the agent tools, such as `No agent tools` for the baseline." }),
|
|
850
906
|
skills: libraryIds("skills"),
|
|
851
907
|
mcp_servers: libraryIds("MCP servers"),
|
|
852
|
-
command_line_tools: libraryIds("command-line tools")
|
|
908
|
+
command_line_tools: libraryIds("command-line tools"),
|
|
909
|
+
browser: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))).annotate({ description: "Whether agents without a browser of their own get one, a headless Chromium in their sandbox whose pages can offer them tools through WebMCP. The Codex app and Claude Cowork always use their own browser. Defaults to false." })
|
|
853
910
|
}).annotate({
|
|
854
911
|
identifier: "AgentTools",
|
|
855
|
-
description: "Skills, MCP servers
|
|
912
|
+
description: "Skills, MCP servers, command-line tools and a browser given to the agent. Keep one with none as the baseline the others are compared to."
|
|
856
913
|
});
|
|
857
914
|
const ExcludedVariant = Schema.Struct({
|
|
858
915
|
repository_id: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Id of one of the experiment's repositories. Omit it for no repository." })),
|
|
@@ -943,7 +1000,7 @@ const Execution = Schema.Struct({
|
|
|
943
1000
|
judging: Count.annotate({ description: "Ended runs waiting for their evaluation results." }),
|
|
944
1001
|
judged: Count.annotate({ description: "Runs with a verdict." }),
|
|
945
1002
|
passed: Count.annotate({ description: "Runs that passed their scenario." })
|
|
946
|
-
}).annotate({ description: "How many of its
|
|
1003
|
+
}).annotate({ description: "How many of its slots, one per scenario, variant, agent and repetition, stand where." }),
|
|
947
1004
|
pass_rate: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
948
1005
|
minimum: 0,
|
|
949
1006
|
maximum: 100
|
|
@@ -1138,7 +1195,10 @@ var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiC
|
|
|
1138
1195
|
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
|
|
1139
1196
|
const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
|
|
1140
1197
|
return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
|
|
1141
|
-
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(
|
|
1198
|
+
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(Option.match(options.apiKey, {
|
|
1199
|
+
onNone: () => request,
|
|
1200
|
+
onSome: (apiKey) => HttpClientRequest.bearerToken(request, Redacted.value(apiKey))
|
|
1201
|
+
})))));
|
|
1142
1202
|
};
|
|
1143
1203
|
// ../env/src/env.ts
|
|
1144
1204
|
const Secret = Schema.Redacted(Schema.NonEmptyString);
|
|
@@ -1152,7 +1212,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
|
1152
1212
|
"127.0.0.1",
|
|
1153
1213
|
"[::1]"
|
|
1154
1214
|
]);
|
|
1155
|
-
const
|
|
1215
|
+
const TRACKS_PUBLIC_API_URL = `https://tracks.alpic.ai/api`;
|
|
1156
1216
|
const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
|
|
1157
1217
|
const ENCRYPTION_KEY_BYTES = 32;
|
|
1158
1218
|
const EncryptionKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.makeFilter((key) => Buffer.from(key, "base64").length === ENCRYPTION_KEY_BYTES || `must be ${ENCRYPTION_KEY_BYTES} random bytes in base64: openssl rand -base64 ${ENCRYPTION_KEY_BYTES}`)));
|
|
@@ -1183,11 +1243,12 @@ const Secrets = Schema.Struct({
|
|
|
1183
1243
|
IDENTITY_PROVIDER_CLIENT_ID: Secret,
|
|
1184
1244
|
IDENTITY_PROVIDER_CLIENT_SECRET: Secret,
|
|
1185
1245
|
SESSION_SECRET: Secret,
|
|
1246
|
+
VERCEL_CLIENT_SECRET: Secret,
|
|
1186
1247
|
SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
|
|
1187
1248
|
});
|
|
1188
1249
|
const Variables = Schema.Struct({
|
|
1189
1250
|
PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
|
|
1190
|
-
TRACKS_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(
|
|
1251
|
+
TRACKS_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(TRACKS_PUBLIC_API_URL))),
|
|
1191
1252
|
DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
|
|
1192
1253
|
AGENT_RUNNER_URL: SecureUrl,
|
|
1193
1254
|
FILES_BUCKET: Schema.NonEmptyString,
|
|
@@ -1214,6 +1275,8 @@ const Variables = Schema.Struct({
|
|
|
1214
1275
|
POSTHOG_KEY: Schema.optionalKey(Schema.NonEmptyString),
|
|
1215
1276
|
POSTHOG_HOST: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://us.i.posthog.com"))),
|
|
1216
1277
|
CLOUDFLARE_CLIENT_ID: Schema.NonEmptyString,
|
|
1278
|
+
VERCEL_CLIENT_ID: Schema.NonEmptyString,
|
|
1279
|
+
VERCEL_INTEGRATION_SLUG: Schema.NonEmptyString,
|
|
1217
1280
|
IDENTITY_PROVIDER_URL: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://idp.alpic.ai")))
|
|
1218
1281
|
});
|
|
1219
1282
|
const Env = Schema.Struct({
|
|
@@ -1250,7 +1313,13 @@ const IDENTITY_ENV = [
|
|
|
1250
1313
|
"IDENTITY_PROVIDER_CLIENT_SECRET",
|
|
1251
1314
|
"SESSION_SECRET"
|
|
1252
1315
|
];
|
|
1253
|
-
const TRAFFIC_ENV = [
|
|
1316
|
+
const TRAFFIC_ENV = [
|
|
1317
|
+
"CLOUDFLARE_CLIENT_ID",
|
|
1318
|
+
"VERCEL_CLIENT_ID",
|
|
1319
|
+
"VERCEL_INTEGRATION_SLUG",
|
|
1320
|
+
"VERCEL_CLIENT_SECRET",
|
|
1321
|
+
"TRAFFIC_ENCRYPTION_KEY"
|
|
1322
|
+
];
|
|
1254
1323
|
const AGENT_RUNNER_ENV = [
|
|
1255
1324
|
"FILES_BUCKET",
|
|
1256
1325
|
"FILES_ENDPOINT",
|
|
@@ -1280,12 +1349,11 @@ const SERVICE_ENV = [
|
|
|
1280
1349
|
...IDENTITY_ENV,
|
|
1281
1350
|
...TRAFFIC_ENV
|
|
1282
1351
|
];
|
|
1283
|
-
const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
|
|
1284
1352
|
const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
|
|
1285
1353
|
SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
|
|
1286
1354
|
const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
|
|
1287
1355
|
// package.json
|
|
1288
|
-
var version = "0.
|
|
1356
|
+
var version = "0.10.0";
|
|
1289
1357
|
// src/connections.ts
|
|
1290
1358
|
const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
|
|
1291
1359
|
const encodeConnections = Schema.encodeEffect(Schema.fromJsonString(List(Connection), { space: 2 }));
|
|
@@ -1369,7 +1437,7 @@ const create$2 = Command.make("create", { server: Argument.String("server-url").
|
|
|
1369
1437
|
});
|
|
1370
1438
|
yield* Console.log(yield* encodeConnection(connection));
|
|
1371
1439
|
}, 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"));
|
|
1372
|
-
const list$
|
|
1440
|
+
const list$2 = Command.make("list", {}, Effect.fn("connections.list")(function* () {
|
|
1373
1441
|
const client = yield* ApiClient;
|
|
1374
1442
|
yield* Console.log(yield* encodeConnections(yield* client.connections.list()));
|
|
1375
1443
|
})).pipe(Command.withDescription("List connections"));
|
|
@@ -1382,7 +1450,7 @@ const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn(
|
|
|
1382
1450
|
})).pipe(Command.withDescription("Delete a connection and the access it holds"));
|
|
1383
1451
|
const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
|
|
1384
1452
|
create$2,
|
|
1385
|
-
list$
|
|
1453
|
+
list$2,
|
|
1386
1454
|
get$2,
|
|
1387
1455
|
deleteConnection
|
|
1388
1456
|
]));
|
|
@@ -1396,6 +1464,10 @@ const experimentId = Argument.String("id").pipe(Argument.withDescription("Experi
|
|
|
1396
1464
|
const executionId = Argument.String("id").pipe(Argument.withDescription("Execution id"), Argument.withSchema(ExecutionId));
|
|
1397
1465
|
const definition = Argument.FileSchema("file", ExperimentInput).pipe(Argument.withDescription("JSON file of the experiment, as `tracks experiments get` prints it, without its id"));
|
|
1398
1466
|
const POLL_INTERVAL = "10 seconds";
|
|
1467
|
+
const POLL_RETRIES = {
|
|
1468
|
+
schedule: Schedule.exponential("2 seconds").pipe(Schedule.jittered),
|
|
1469
|
+
times: 5
|
|
1470
|
+
};
|
|
1399
1471
|
var PassRateTooLow = class extends Schema.TaggedError()("PassRateTooLow", {
|
|
1400
1472
|
passRate: Schema.Option(Schema.Number),
|
|
1401
1473
|
minimum: Schema.Number
|
|
@@ -1412,7 +1484,7 @@ const create$1 = Command.make("create", { definition }, Effect.fn("experiments.c
|
|
|
1412
1484
|
const client = yield* ApiClient;
|
|
1413
1485
|
yield* Console.log(yield* encodeExperiment(yield* client.experiments.create({ payload })));
|
|
1414
1486
|
})).pipe(Command.withDescription("Create an experiment from a JSON file"));
|
|
1415
|
-
const list = Command.make("list", {}, Effect.fn("experiments.list")(function* () {
|
|
1487
|
+
const list$1 = Command.make("list", {}, Effect.fn("experiments.list")(function* () {
|
|
1416
1488
|
const client = yield* ApiClient;
|
|
1417
1489
|
yield* Console.log(yield* encodeExperiments(yield* client.experiments.list()));
|
|
1418
1490
|
})).pipe(Command.withDescription("List experiments"));
|
|
@@ -1439,16 +1511,19 @@ const launch = Command.make("launch", {
|
|
|
1439
1511
|
minPassRate: Flag.Int("min-pass-rate").pipe(Flag.withSchema(Schema.Int.check(Schema.isBetween({
|
|
1440
1512
|
minimum: 0,
|
|
1441
1513
|
maximum: 100
|
|
1442
|
-
}))), Flag.withDescription("Wait, then exit with
|
|
1514
|
+
}))), Flag.withDescription("Wait, then exit with code 2 when the pass rate ends below this percentage, from 0 to 100"), Flag.optional)
|
|
1443
1515
|
}, Effect.fn("experiments.launch")(function* ({ id, wait, minPassRate }) {
|
|
1444
1516
|
const client = yield* ApiClient;
|
|
1445
1517
|
const launched = yield* client.executions.launch({ params: { id } });
|
|
1446
1518
|
if (!wait && Option.isNone(minPassRate)) return yield* Console.log(yield* encodeExecution(launched));
|
|
1447
1519
|
yield* Console.error(`Launched execution ${launched.id}: ${launched.slots.total} runs`);
|
|
1448
|
-
const done = yield* client.executions.get({ params: { id: launched.id } }).pipe(Effect.
|
|
1520
|
+
const done = yield* client.executions.get({ params: { id: launched.id } }).pipe(Effect.retry({
|
|
1521
|
+
...POLL_RETRIES,
|
|
1522
|
+
while: (error) => error._tag === "HttpClientError" && error.reason._tag === "TransportError"
|
|
1523
|
+
}), Effect.tap(({ slots }) => Console.error(`${slots.running} running, ${slots.judging} judging, ${slots.judged} judged`)), Effect.repeat({
|
|
1449
1524
|
schedule: Schedule.spaced(POLL_INTERVAL),
|
|
1450
1525
|
until: (execution) => !execution.pending
|
|
1451
|
-
}));
|
|
1526
|
+
}), Effect.tapErrorTag("HttpClientError", () => Console.error(`Execution ${launched.id} keeps running: check it with tracks executions get ${launched.id}`)));
|
|
1452
1527
|
yield* Console.log(yield* encodeExecution(done));
|
|
1453
1528
|
if (Option.isSome(minPassRate) && !Option.exists(done.pass_rate, (rate) => rate >= minPassRate.value)) return yield* new PassRateTooLow({
|
|
1454
1529
|
passRate: done.pass_rate,
|
|
@@ -1461,7 +1536,7 @@ const executionsOfExperiment = Command.make("executions", { id: experimentId },
|
|
|
1461
1536
|
})).pipe(Command.withDescription("List an experiment's executions"));
|
|
1462
1537
|
const experiments = Command.make("experiments").pipe(Command.withDescription("Compare agents on your scenarios"), Command.withSubcommands([
|
|
1463
1538
|
create$1,
|
|
1464
|
-
list,
|
|
1539
|
+
list$1,
|
|
1465
1540
|
get$1,
|
|
1466
1541
|
update,
|
|
1467
1542
|
deleteExperiment,
|
|
@@ -1562,6 +1637,7 @@ const fixedHeaderOf = (entry) => {
|
|
|
1562
1637
|
value: entry.slice(colon + 1).trim()
|
|
1563
1638
|
});
|
|
1564
1639
|
};
|
|
1640
|
+
const labelFlag = (description) => Flag.String("label").pipe(Flag.withDescription(description), Flag.atLeast(0), Flag.withSchema(LabelFilters));
|
|
1565
1641
|
var FixedHeaderUnmatched = class extends Schema.TaggedError()("FixedHeaderUnmatched", { server: Schema.String }) {
|
|
1566
1642
|
get message() {
|
|
1567
1643
|
return `--mcp-header ${this.server}=… needs an --mcp ${this.server}=URL`;
|
|
@@ -1586,12 +1662,14 @@ const create = Command.make("create", {
|
|
|
1586
1662
|
})),
|
|
1587
1663
|
onSome: Effect.succeed
|
|
1588
1664
|
})), Flag.atLeast(0)),
|
|
1665
|
+
browser: Flag.Boolean("browser").pipe(Flag.withDescription("Give the agent a browser, a headless Chromium in its sandbox whose pages can offer it tools through WebMCP. The Codex app and Claude Cowork always use their own browser instead"), Flag.withDefault(false)),
|
|
1589
1666
|
budget: Flag.Finite("budget").pipe(Flag.withAlias("b"), Flag.withDescription("Most the run may spend, in US dollars"), Flag.optional),
|
|
1590
1667
|
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),
|
|
1591
1668
|
idleTimeout: Flag.Int("idle-timeout").pipe(Flag.withDescription("Seconds an idle run waits for a follow-up before it ends"), Flag.optional),
|
|
1592
1669
|
timeLimit: Flag.Int("time-limit").pipe(Flag.withDescription("Longest the run may last, in seconds, before it is stopped"), Flag.optional),
|
|
1593
|
-
idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional)
|
|
1594
|
-
|
|
1670
|
+
idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional),
|
|
1671
|
+
label: labelFlag("Label attached to the run to find it again, as KEY:VALUE, such as ticket:ENG-42; repeat for several")
|
|
1672
|
+
}, Effect.fn("runs.create")(function* ({ prompt, harness, model, file, repository, env, setup, teardown, mcp, mcpHeader, browser, budget, oneShot, idleTimeout, timeLimit, idempotencyKey, label }) {
|
|
1595
1673
|
const unmatched = mcpHeader.find((header) => !mcp.some((server) => server.url !== void 0 && server.name === header.server));
|
|
1596
1674
|
if (unmatched !== void 0) return yield* new FixedHeaderUnmatched({ server: unmatched.server });
|
|
1597
1675
|
const mcpServers = mcp.map((server) => {
|
|
@@ -1618,16 +1696,31 @@ const create = Command.make("create", {
|
|
|
1618
1696
|
setup,
|
|
1619
1697
|
teardown,
|
|
1620
1698
|
skills: Option.none(),
|
|
1621
|
-
mcp_servers: Option.some(mcpServers)
|
|
1699
|
+
mcp_servers: Option.some(mcpServers),
|
|
1700
|
+
browser
|
|
1622
1701
|
},
|
|
1623
1702
|
budget_usd: budget,
|
|
1624
1703
|
one_shot: oneShot,
|
|
1625
1704
|
idle_timeout_seconds: idleTimeout,
|
|
1626
|
-
time_limit_seconds: timeLimit
|
|
1705
|
+
time_limit_seconds: timeLimit,
|
|
1706
|
+
labels: label
|
|
1627
1707
|
}
|
|
1628
1708
|
});
|
|
1629
1709
|
yield* Console.log(yield* encodeRun(run));
|
|
1630
1710
|
})).pipe(Command.withDescription("Start a run"));
|
|
1711
|
+
const encodeRuns = Schema.encodeEffect(Schema.fromJsonString(List(Run), { space: 2 }));
|
|
1712
|
+
const list = Command.make("list", {
|
|
1713
|
+
label: labelFlag("Only runs with this label, as KEY:VALUE; repeat to require several"),
|
|
1714
|
+
after: Flag.String("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only runs listed after the run with this id"), Flag.withSchema(RunId), Flag.optional),
|
|
1715
|
+
limit: Flag.Int("limit").pipe(Flag.withAlias("l"), Flag.withDescription("Maximum runs returned"), Flag.optional)
|
|
1716
|
+
}, Effect.fn("runs.list")(function* ({ label, after, limit }) {
|
|
1717
|
+
const page = yield* (yield* ApiClient).runs.list({ query: {
|
|
1718
|
+
label: Object.keys(label).length === 0 ? Option.none() : Option.some(label),
|
|
1719
|
+
after,
|
|
1720
|
+
limit
|
|
1721
|
+
} });
|
|
1722
|
+
yield* Console.log(yield* encodeRuns(page));
|
|
1723
|
+
})).pipe(Command.withDescription("List your runs, most recently started first"));
|
|
1631
1724
|
const get = Command.make("get", { id: runId }, Effect.fn("runs.get")(function* ({ id }) {
|
|
1632
1725
|
const run = yield* (yield* ApiClient).runs.get({ params: { id } });
|
|
1633
1726
|
yield* Console.log(yield* encodeRun(run));
|
|
@@ -1669,6 +1762,14 @@ const events = Command.make("events", {
|
|
|
1669
1762
|
yield* Console.log(yield* encodeEvents(page));
|
|
1670
1763
|
})).pipe(Command.withDescription("List a run's events"));
|
|
1671
1764
|
const printed = (sent, itemOf, encode) => sent.pipe(Stream.filterMap(itemOf), Stream.tap((item) => Effect.flatMap(encode(item), Console.log)), Stream.map((item) => item.seq), Stream.runLast);
|
|
1765
|
+
var RunUnfinished = class extends Schema.TaggedError()("RunUnfinished", {
|
|
1766
|
+
id: RunId,
|
|
1767
|
+
endReason: Schema.String
|
|
1768
|
+
}) {
|
|
1769
|
+
get message() {
|
|
1770
|
+
return `Run ${this.id} ended with ${this.endReason} before its agent finished`;
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1672
1773
|
const follow = Command.make("follow", {
|
|
1673
1774
|
id: runId,
|
|
1674
1775
|
after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only frames, or events with --events, with a greater seq"), Flag.optional),
|
|
@@ -1685,9 +1786,15 @@ const follow = Command.make("follow", {
|
|
|
1685
1786
|
let lastSeen = after;
|
|
1686
1787
|
while (true) {
|
|
1687
1788
|
lastSeen = Option.orElse(yield* followOnce(lastSeen), () => lastSeen);
|
|
1688
|
-
|
|
1789
|
+
const run = yield* client.runs.get({ params: { id } });
|
|
1790
|
+
if (run.status !== "ended") continue;
|
|
1791
|
+
if (run.end_reason === "completed" || run.end_reason === "idle_timeout") return;
|
|
1792
|
+
return yield* new RunUnfinished({
|
|
1793
|
+
id,
|
|
1794
|
+
endReason: run.end_reason
|
|
1795
|
+
});
|
|
1689
1796
|
}
|
|
1690
|
-
})).pipe(Command.withDescription("Print a run's frames, or its events with --events, as they come, one JSON object per line, until the run ends"));
|
|
1797
|
+
})).pipe(Command.withDescription("Print a run's frames, or its events with --events, as they come, one JSON object per line, until the run ends; exit with code 2 when it ended before its agent finished"));
|
|
1691
1798
|
const sendPrompt = Command.make("send-prompt", {
|
|
1692
1799
|
id: runId,
|
|
1693
1800
|
text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
|
|
@@ -1726,6 +1833,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
|
|
|
1726
1833
|
})).pipe(Command.withDescription("Stop a run right away, even mid-prompt"));
|
|
1727
1834
|
const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
|
|
1728
1835
|
create,
|
|
1836
|
+
list,
|
|
1729
1837
|
get,
|
|
1730
1838
|
sendPrompt,
|
|
1731
1839
|
prompts,
|
|
@@ -1736,12 +1844,12 @@ const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), C
|
|
|
1736
1844
|
end,
|
|
1737
1845
|
cancel
|
|
1738
1846
|
]));
|
|
1739
|
-
// src/
|
|
1847
|
+
// src/tracks.ts
|
|
1740
1848
|
const health = Command.make("health", {}, Effect.fn("health")(function* () {
|
|
1741
1849
|
yield* (yield* ApiClient).health();
|
|
1742
1850
|
yield* Console.log("ok");
|
|
1743
1851
|
})).pipe(Command.withDescription("Check that the API is reachable"));
|
|
1744
|
-
const
|
|
1852
|
+
const tracks = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes and compare them on your scenarios"), Command.withSubcommands([
|
|
1745
1853
|
health,
|
|
1746
1854
|
runs,
|
|
1747
1855
|
connections,
|
|
@@ -1749,15 +1857,23 @@ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agen
|
|
|
1749
1857
|
executions,
|
|
1750
1858
|
library
|
|
1751
1859
|
]));
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
}))
|
|
1860
|
+
// src/main.ts
|
|
1861
|
+
const apiKey = Effect.option(Effect.gen(function* () {
|
|
1862
|
+
const { TRACKS_API_KEY } = yield* readEnv("TRACKS_API_KEY");
|
|
1863
|
+
return TRACKS_API_KEY;
|
|
1864
|
+
}));
|
|
1865
|
+
const ApiClientLayer = Layer.unwrap(Effect.gen(function* () {
|
|
1866
|
+
const { TRACKS_API_URL } = yield* readEnv("TRACKS_API_URL");
|
|
1867
|
+
return ApiClient.layer({
|
|
1868
|
+
baseUrl: TRACKS_API_URL,
|
|
1869
|
+
apiKey: yield* apiKey,
|
|
1870
|
+
userAgent: `${CLI_USER_AGENT}/${version}`
|
|
1871
|
+
});
|
|
1872
|
+
}));
|
|
1757
1873
|
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer).pipe(Layer.provideMerge(FetchHttpClient.layer));
|
|
1758
|
-
Command.run(
|
|
1874
|
+
Command.run(tracks, { version }).pipe(Effect.catch((error) => {
|
|
1759
1875
|
const explainedByHelp = error._tag === "ShowHelp";
|
|
1760
1876
|
const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
|
|
1761
|
-
return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = 1)));
|
|
1877
|
+
return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = error._tag === "RunUnfinished" || error._tag === "PassRateTooLow" ? 2 : 1)));
|
|
1762
1878
|
}), Effect.provide(MainLayer), NodeRuntime.runMain);
|
|
1763
1879
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/tracks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Run coding agents in sandboxes from the command line",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"effect": "^4.0.7"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@effect/vitest": "^4.0.7",
|
|
27
28
|
"@tracks/contract": "0.0.0",
|
|
29
|
+
"@tracks/docs": "0.0.0",
|
|
28
30
|
"@tracks/env": "0.0.0",
|
|
29
31
|
"rolldown": "^1.2.15"
|
|
30
32
|
},
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
37
|
"start": "node --import @tracks/env/preload src/main.ts",
|
|
36
|
-
"build": "rolldown --config rolldown.config.ts"
|
|
38
|
+
"build": "rolldown --config rolldown.config.ts",
|
|
39
|
+
"docs:generate": "node scripts/docs.ts"
|
|
37
40
|
}
|
|
38
41
|
}
|