@alpic-ai/tracks 0.6.0 → 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.
Files changed (2) hide show
  1. package/dist/main.js +505 -59
  2. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -1,8 +1,8 @@
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
7
  import { Argument, CliError, Command, Flag } from "effect/cli";
8
8
  import { createServer } from "node:http";
@@ -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
- //#endregion
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
- //#endregion
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."
@@ -144,20 +142,10 @@ 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
- //#endregion
148
- //#region ../contract/src/files.ts
145
+ // ../contract/src/files.ts
149
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." });
150
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." });
151
- const FileContentType = Schema.Literals([
152
- "image/png",
153
- "image/jpeg",
154
- "image/webp",
155
- "image/gif",
156
- "application/pdf",
157
- "text/plain",
158
- "text/csv",
159
- "application/json"
160
- ]);
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." });
161
149
  const FileSize = Schema.Int.check(Schema.isBetween({
162
150
  minimum: 1,
163
151
  maximum: 20971520
@@ -192,24 +180,24 @@ var FilesApi = class extends HttpApiGroup.make("files").add(HttpApiEndpoint.post
192
180
  }).pipe(HttpApiSchema.status(201))
193
181
  }).annotateMerge(OpenApi.annotations({
194
182
  summary: "Request a file upload",
195
- description: "Request with `name`, `content_type` and exact byte `size`, e.g. `{\"name\":\"photo.png\",\"content_type\":\"image/png\",\"size\":12345}`. Supports PNG, JPEG, WebP, GIF, PDF, text, CSV and JSON, up to 20 MiB per file. 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."
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."
196
184
  }))).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
197
- //#endregion
198
- //#region ../contract/src/runs.ts
185
+ // ../contract/src/runs.ts
199
186
  const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
200
187
  identifier: "RunId",
201
188
  description: "Unique identifier of the run."
202
189
  });
203
190
  const Harness = Schema.Literals([
204
191
  "claude-code",
192
+ "claude-desktop",
205
193
  "codex",
206
194
  "codex-app",
207
195
  "cursor"
208
196
  ]).annotate({
209
197
  identifier: "Harness",
210
- 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)."
211
199
  });
212
- const Agent = Schema.Struct({
200
+ const Agent$1 = Schema.Struct({
213
201
  harness: Harness,
214
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`." })
215
203
  }).annotate({
@@ -392,16 +380,17 @@ const LabelFilters = Schema.Array(Schema.String.check(Schema.isPattern(LABEL_FIL
392
380
  decode: (filters) => Object.fromEntries(filters.map(keyAndValueOf)),
393
381
  encode: (labels) => Object.entries(labels).map(([key, value]) => `${key}:${value}`)
394
382
  })));
383
+ const MAX_BUDGET_USD = 1e4;
395
384
  const Budget = Schema.Finite.check(Schema.isBetween({
396
385
  minimum: .01,
397
- maximum: 1e4
386
+ maximum: MAX_BUDGET_USD
398
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." });
399
388
  const MIN_IDLE_TIMEOUT_SECONDS = 120;
400
389
  const MAX_IDLE_TIMEOUT_SECONDS = 900;
401
390
  const MIN_TIME_LIMIT_SECONDS = 60;
402
391
  const MAX_TIME_LIMIT_SECONDS = 43200;
403
392
  const CreateRunInput = Schema.Struct({
404
- agent: Agent,
393
+ agent: Agent$1,
405
394
  prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
406
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." })),
407
396
  environment: EnvironmentInput,
@@ -477,7 +466,7 @@ const runFields = {
477
466
  id: RunId,
478
467
  input_files: Schema.Array(InputFile),
479
468
  labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
480
- agent: Agent,
469
+ agent: Agent$1,
481
470
  environment: RunEnvironment,
482
471
  budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
483
472
  one_shot: Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes." }),
@@ -557,13 +546,13 @@ var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Sch
557
546
  httpApiStatus: 409,
558
547
  description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
559
548
  }) {};
560
- const Count = Schema.Natural;
549
+ const Count$1 = Schema.Natural;
561
550
  const RunUsage = Schema.Struct({
562
551
  cost_usd: Schema.Number.annotate({ description: "Total cost of the run so far, in US dollars." }),
563
552
  models: Schema.Array(Schema.Struct({
564
553
  provider: Schema.String.annotate({ description: "Company serving the model, such as `anthropic`." }),
565
554
  model: Schema.String,
566
- 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." }),
567
556
  cost_usd: Schema.Number.annotate({ description: "Cost of this model's usage, in US dollars." })
568
557
  })).annotate({ description: "Model usage, one entry per model." }),
569
558
  sandbox: Schema.Struct({
@@ -808,8 +797,328 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
808
797
  title: "Runs",
809
798
  description: "Start coding agents and follow their work."
810
799
  })).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
811
- //#endregion
812
- //#region ../contract/src/system.ts
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
813
1122
  const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
814
1123
  var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
815
1124
  summary: "Check health",
@@ -818,14 +1127,12 @@ var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).ad
818
1127
  summary: "List egress IPs",
819
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."
820
1129
  }))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
821
- //#endregion
822
- //#region ../contract/src/api.ts
823
- var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(FilesApi).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({
824
1132
  title: "Tracks API",
825
- 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."
826
1134
  })) {};
827
- //#endregion
828
- //#region ../contract/src/client.ts
1135
+ // ../contract/src/client.ts
829
1136
  const CLI_USER_AGENT = "tracks";
830
1137
  var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
831
1138
  static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
@@ -833,8 +1140,7 @@ var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiC
833
1140
  return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
834
1141
  }) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
835
1142
  };
836
- //#endregion
837
- //#region ../env/src/env.ts
1143
+ // ../env/src/env.ts
838
1144
  const Secret = Schema.Redacted(Schema.NonEmptyString);
839
1145
  const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
840
1146
  const Port = Schema.Int.check(Schema.isBetween({
@@ -871,6 +1177,7 @@ const Secrets = Schema.Struct({
871
1177
  TYPESAFE_API_KEY: Secret,
872
1178
  DATABASE_PASSWORD: Secret,
873
1179
  CONNECTION_ENCRYPTION_KEY: EncryptionKey,
1180
+ TRAFFIC_ENCRYPTION_KEY: EncryptionKey,
874
1181
  STRIPE_SECRET_KEY: StripeKey("rk_"),
875
1182
  STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
876
1183
  IDENTITY_PROVIDER_CLIENT_ID: Secret,
@@ -904,7 +1211,10 @@ const Variables = Schema.Struct({
904
1211
  DEPLOYMENT_ENVIRONMENT: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("local"))),
905
1212
  OTLP_METRICS_URL: Schema.optionalKey(Schema.URLFromString),
906
1213
  RELEASE: Schema.optionalKey(Schema.NonEmptyString),
907
- IDENTITY_PROVIDER_URL: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://id.alpic.ai")))
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")))
908
1218
  });
909
1219
  const Env = Schema.Struct({
910
1220
  ...Secrets.fields,
@@ -940,6 +1250,7 @@ const IDENTITY_ENV = [
940
1250
  "IDENTITY_PROVIDER_CLIENT_SECRET",
941
1251
  "SESSION_SECRET"
942
1252
  ];
1253
+ const TRAFFIC_ENV = ["CLOUDFLARE_CLIENT_ID", "TRAFFIC_ENCRYPTION_KEY"];
943
1254
  const AGENT_RUNNER_ENV = [
944
1255
  "FILES_BUCKET",
945
1256
  "FILES_ENDPOINT",
@@ -952,6 +1263,8 @@ const AGENT_RUNNER_ENV = [
952
1263
  const LOG_ENV = ["LOG_FORMAT", "LOG_LEVEL"];
953
1264
  const OBSERVABILITY_ENV = [
954
1265
  "SENTRY_DSN",
1266
+ "POSTHOG_KEY",
1267
+ "POSTHOG_HOST",
955
1268
  "DEPLOYMENT_ENVIRONMENT",
956
1269
  "OTLP_METRICS_URL",
957
1270
  "RELEASE"
@@ -964,17 +1277,16 @@ const SERVICE_ENV = [
964
1277
  ...OBSERVABILITY_ENV,
965
1278
  ...AGENT_RUNNER_ENV,
966
1279
  ...PAYMENTS_ENV,
967
- ...IDENTITY_ENV
1280
+ ...IDENTITY_ENV,
1281
+ ...TRAFFIC_ENV
968
1282
  ];
969
1283
  const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
970
1284
  const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
971
1285
  SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
972
1286
  const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
973
- //#endregion
974
- //#region package.json
975
- var version = "0.6.0";
976
- //#endregion
977
- //#region src/connections.ts
1287
+ // package.json
1288
+ var version = "0.7.1";
1289
+ // src/connections.ts
978
1290
  const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
979
1291
  const encodeConnections = Schema.encodeEffect(Schema.fromJsonString(List(Connection), { space: 2 }));
980
1292
  const connectionId = Argument.String("id").pipe(Argument.withDescription("Connection id"), Argument.withSchema(ConnectionId));
@@ -1037,7 +1349,7 @@ const confirmationOf = (params, id) => {
1037
1349
  onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
1038
1350
  });
1039
1351
  };
1040
- const create$1 = Command.make("create", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.create")(function* ({ server }) {
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 }) {
1041
1353
  const client = yield* ApiClient;
1042
1354
  const callback = yield* loopbackCallback;
1043
1355
  const created = yield* client.connections.create({ payload: {
@@ -1057,11 +1369,11 @@ const create$1 = Command.make("create", { server: Argument.String("server-url").
1057
1369
  });
1058
1370
  yield* Console.log(yield* encodeConnection(connection));
1059
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"));
1060
- const list = Command.make("list", {}, Effect.fn("connections.list")(function* () {
1372
+ const list$1 = Command.make("list", {}, Effect.fn("connections.list")(function* () {
1061
1373
  const client = yield* ApiClient;
1062
1374
  yield* Console.log(yield* encodeConnections(yield* client.connections.list()));
1063
1375
  })).pipe(Command.withDescription("List connections"));
1064
- const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.get")(function* ({ id }) {
1376
+ const get$2 = Command.make("get", { id: connectionId }, Effect.fn("connections.get")(function* ({ id }) {
1065
1377
  const client = yield* ApiClient;
1066
1378
  yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
1067
1379
  })).pipe(Command.withDescription("Show a connection"));
@@ -1069,13 +1381,143 @@ const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn(
1069
1381
  yield* (yield* ApiClient).connections.delete({ params: { id } });
1070
1382
  })).pipe(Command.withDescription("Delete a connection and the access it holds"));
1071
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([
1072
1463
  create$1,
1073
1464
  list,
1074
1465
  get$1,
1075
- deleteConnection
1466
+ update,
1467
+ deleteExperiment,
1468
+ launch,
1469
+ executionsOfExperiment
1470
+ ]));
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
1076
1487
  ]));
1077
- //#endregion
1078
- //#region src/runs.ts
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
1079
1521
  const encodeRun = Schema.encodeEffect(Schema.fromJsonString(Run, { space: 2 }));
1080
1522
  const encodeFrames = Schema.encodeEffect(Schema.fromJsonString(List(Frame), { space: 2 }));
1081
1523
  const encodeEvents = Schema.encodeEffect(Schema.fromJsonString(List(Event), { space: 2 }));
@@ -1129,6 +1571,7 @@ const create = Command.make("create", {
1129
1571
  prompt: Argument.String("prompt").pipe(Argument.withDescription("Prompt sent to the agent")),
1130
1572
  harness: Flag.Literals("harness", Harness.literals).pipe(Flag.withAlias("H"), Flag.withDescription("Harness running the agent")),
1131
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)),
1132
1575
  repository: Flag.String("repository").pipe(Flag.withAlias("r"), Flag.withDescription("Git repository cloned into the sandbox"), Flag.optional),
1133
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)),
1134
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),
@@ -1148,7 +1591,7 @@ const create = Command.make("create", {
1148
1591
  idleTimeout: Flag.Int("idle-timeout").pipe(Flag.withDescription("Seconds an idle run waits for a follow-up before it ends"), Flag.optional),
1149
1592
  timeLimit: Flag.Int("time-limit").pipe(Flag.withDescription("Longest the run may last, in seconds, before it is stopped"), Flag.optional),
1150
1593
  idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional)
1151
- }, Effect.fn("runs.create")(function* ({ prompt, harness, model, repository, env, setup, teardown, mcp, mcpHeader, 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 }) {
1152
1595
  const unmatched = mcpHeader.find((header) => !mcp.some((server) => server.url !== void 0 && server.name === header.server));
1153
1596
  if (unmatched !== void 0) return yield* new FixedHeaderUnmatched({ server: unmatched.server });
1154
1597
  const mcpServers = mcp.map((server) => {
@@ -1159,6 +1602,7 @@ const create = Command.make("create", {
1159
1602
  headers: Option.some(Object.fromEntries(headers.map(({ name, value }) => [name, value])))
1160
1603
  };
1161
1604
  });
1605
+ const inputFiles = yield* Effect.forEach(file, uploadFile, { concurrency: "unbounded" });
1162
1606
  const run = yield* (yield* ApiClient).runs.create({
1163
1607
  headers: { "idempotency-key": idempotencyKey },
1164
1608
  payload: {
@@ -1167,6 +1611,7 @@ const create = Command.make("create", {
1167
1611
  model
1168
1612
  },
1169
1613
  prompt,
1614
+ input_files: inputFiles,
1170
1615
  environment: {
1171
1616
  repository,
1172
1617
  variables: Option.some(Object.fromEntries(env.map(({ name, value }) => [name, value]))),
@@ -1291,27 +1736,28 @@ const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), C
1291
1736
  end,
1292
1737
  cancel
1293
1738
  ]));
1294
- //#endregion
1295
- //#region src/main.ts
1739
+ // src/main.ts
1296
1740
  const health = Command.make("health", {}, Effect.fn("health")(function* () {
1297
1741
  yield* (yield* ApiClient).health();
1298
1742
  yield* Console.log("ok");
1299
1743
  })).pipe(Command.withDescription("Check that the API is reachable"));
1300
- 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([
1301
1745
  health,
1302
1746
  runs,
1303
- connections
1747
+ connections,
1748
+ experiments,
1749
+ executions,
1750
+ library
1304
1751
  ]));
1305
1752
  const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
1306
1753
  baseUrl: env.TRACKS_API_URL,
1307
1754
  apiKey: env.TRACKS_API_KEY,
1308
1755
  userAgent: `${CLI_USER_AGENT}/${version}`
1309
- }))).pipe(Layer.provide(FetchHttpClient.layer));
1310
- const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
1756
+ })));
1757
+ const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer).pipe(Layer.provideMerge(FetchHttpClient.layer));
1311
1758
  Command.run(cli, { version }).pipe(Effect.catch((error) => {
1312
1759
  const explainedByHelp = error._tag === "ShowHelp";
1313
1760
  const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
1314
1761
  return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = 1)));
1315
1762
  }), Effect.provide(MainLayer), NodeRuntime.runMain);
1316
- //#endregion
1317
1763
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/tracks",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Run coding agents in sandboxes from the command line",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -20,13 +20,13 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@effect/platform-node": "^4.0.2",
24
- "effect": "^4.0.2"
23
+ "@effect/platform-node": "^4.0.7",
24
+ "effect": "^4.0.7"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@tracks/contract": "0.0.0",
28
28
  "@tracks/env": "0.0.0",
29
- "rolldown": "^1.2.13"
29
+ "rolldown": "^1.2.15"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=24"