@alpic-ai/tracks 0.7.1 → 0.9.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.
Files changed (2) hide show
  1. package/dist/main.js +154 -38
  2. 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
- }))).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
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
- }).annotate({
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's output."
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 and command-line tools given to the agent. Keep one with none as the baseline the others are compared to."
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 planned runs stand where." }),
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(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
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 TRACKS_API_PATH = "/api";
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(`https://agent-runner.lab.alpic.ai${TRACKS_API_PATH}`))),
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 = ["CLOUDFLARE_CLIENT_ID", "TRAFFIC_ENCRYPTION_KEY"];
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.7.1";
1356
+ var version = "0.9.1";
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$1 = Command.make("list", {}, Effect.fn("connections.list")(function* () {
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$1,
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 an error when the pass rate ends below this percentage, from 0 to 100"), Flag.optional)
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.tap(({ slots }) => Console.error(`${slots.running} running, ${slots.judging} judging, ${slots.judged} judged`)), Effect.repeat({
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
- }, Effect.fn("runs.create")(function* ({ prompt, harness, model, file, repository, env, setup, teardown, mcp, mcpHeader, budget, oneShot, idleTimeout, timeLimit, idempotencyKey }) {
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
- if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
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/main.ts
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 cli = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes and compare them on your scenarios"), Command.withSubcommands([
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
- const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
1753
- baseUrl: env.TRACKS_API_URL,
1754
- apiKey: env.TRACKS_API_KEY,
1755
- userAgent: `${CLI_USER_AGENT}/${version}`
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(cli, { version }).pipe(Effect.catch((error) => {
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.7.1",
3
+ "version": "0.9.1",
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
  }