@alpic-ai/tracks 0.6.0 → 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.
- package/dist/main.js +644 -82
- package/package.json +8 -5
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
|
-
|
|
34
|
-
//#region ../contract/src/validation.ts
|
|
33
|
+
// ../contract/src/validation.ts
|
|
35
34
|
const RequestIssue = Schema.Struct({
|
|
36
35
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Number])).annotate({ description: "Keys leading to the wrong value inside that part of the request, such as `[\"agent\", \"model\"]`. Empty when the problem concerns that part as a whole." }),
|
|
37
36
|
message: Schema.String.annotate({ description: "What is wrong with the value." })
|
|
@@ -53,8 +52,7 @@ var InvalidRequest = class extends Schema.TaggedError()("InvalidRequest", {
|
|
|
53
52
|
description: "The request is invalid: a value is missing, has the wrong type or is out of range. Fix it before sending the request again."
|
|
54
53
|
}) {};
|
|
55
54
|
var RequestValidation = class extends HttpApiMiddleware.Service()("@tracks/contract/RequestValidation", { error: InvalidRequest }) {};
|
|
56
|
-
|
|
57
|
-
//#region ../contract/src/connections.ts
|
|
55
|
+
// ../contract/src/connections.ts
|
|
58
56
|
const ConnectionId = Schema.String.pipe(Schema.brand("ConnectionId")).annotate({
|
|
59
57
|
identifier: "ConnectionId",
|
|
60
58
|
description: "Unique identifier of the connection."
|
|
@@ -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
|
-
|
|
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.
|
|
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,27 @@ 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}`.
|
|
196
|
-
}))).
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
185
|
+
title: "Files",
|
|
186
|
+
description: "Upload files to place in a run's sandbox."
|
|
187
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
188
|
+
// ../contract/src/runs.ts
|
|
199
189
|
const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
|
|
200
190
|
identifier: "RunId",
|
|
201
191
|
description: "Unique identifier of the run."
|
|
202
192
|
});
|
|
203
193
|
const Harness = Schema.Literals([
|
|
204
194
|
"claude-code",
|
|
195
|
+
"claude-desktop",
|
|
205
196
|
"codex",
|
|
206
197
|
"codex-app",
|
|
207
198
|
"cursor"
|
|
208
199
|
]).annotate({
|
|
209
200
|
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)."
|
|
201
|
+
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
202
|
});
|
|
212
|
-
const Agent = Schema.Struct({
|
|
203
|
+
const Agent$1 = Schema.Struct({
|
|
213
204
|
harness: Harness,
|
|
214
205
|
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
206
|
}).annotate({
|
|
@@ -333,14 +324,23 @@ const SkillsInput = Schema.Array(SkillInput).check(Schema.isMaxLength(MAX_SKILLS
|
|
|
333
324
|
path: [index, "name"],
|
|
334
325
|
issue: "Expected a name not reserved for the agent"
|
|
335
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";
|
|
336
328
|
const EnvironmentInput = Schema.Struct({
|
|
337
329
|
repository,
|
|
338
330
|
variables: Schema.OptionFromOptionalKey(Variables$1),
|
|
339
331
|
setup,
|
|
340
332
|
teardown,
|
|
341
333
|
skills: Schema.OptionFromOptionalKey(SkillsInput),
|
|
342
|
-
mcp_servers: Schema.OptionFromOptionalKey(McpServersInput)
|
|
343
|
-
|
|
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({
|
|
344
344
|
identifier: "EnvironmentInput",
|
|
345
345
|
description: "What the agent works on."
|
|
346
346
|
});
|
|
@@ -366,7 +366,8 @@ const RunEnvironment = Schema.Struct({
|
|
|
366
366
|
setup,
|
|
367
367
|
teardown,
|
|
368
368
|
skills: Schema.Array(RunSkill),
|
|
369
|
-
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." })
|
|
370
371
|
}).annotate({
|
|
371
372
|
identifier: "Environment",
|
|
372
373
|
description: "What the agent works on."
|
|
@@ -392,16 +393,17 @@ const LabelFilters = Schema.Array(Schema.String.check(Schema.isPattern(LABEL_FIL
|
|
|
392
393
|
decode: (filters) => Object.fromEntries(filters.map(keyAndValueOf)),
|
|
393
394
|
encode: (labels) => Object.entries(labels).map(([key, value]) => `${key}:${value}`)
|
|
394
395
|
})));
|
|
396
|
+
const MAX_BUDGET_USD = 1e4;
|
|
395
397
|
const Budget = Schema.Finite.check(Schema.isBetween({
|
|
396
398
|
minimum: .01,
|
|
397
|
-
maximum:
|
|
399
|
+
maximum: MAX_BUDGET_USD
|
|
398
400
|
})).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
401
|
const MIN_IDLE_TIMEOUT_SECONDS = 120;
|
|
400
402
|
const MAX_IDLE_TIMEOUT_SECONDS = 900;
|
|
401
403
|
const MIN_TIME_LIMIT_SECONDS = 60;
|
|
402
404
|
const MAX_TIME_LIMIT_SECONDS = 43200;
|
|
403
405
|
const CreateRunInput = Schema.Struct({
|
|
404
|
-
agent: Agent,
|
|
406
|
+
agent: Agent$1,
|
|
405
407
|
prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
|
|
406
408
|
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
409
|
environment: EnvironmentInput,
|
|
@@ -465,19 +467,21 @@ const EndReason = Schema.Literals([
|
|
|
465
467
|
const FailureKind = Schema.Literals([
|
|
466
468
|
"repository_checkout_failed",
|
|
467
469
|
"setup_failed",
|
|
470
|
+
"browser_not_started",
|
|
468
471
|
"sandbox_not_found",
|
|
469
472
|
"process_lost",
|
|
470
473
|
"sandbox_unavailable",
|
|
474
|
+
"agent_stopped",
|
|
471
475
|
"unexpected"
|
|
472
476
|
]).annotate({
|
|
473
477
|
identifier: "FailureKind",
|
|
474
|
-
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."
|
|
475
479
|
});
|
|
476
480
|
const runFields = {
|
|
477
481
|
id: RunId,
|
|
478
482
|
input_files: Schema.Array(InputFile),
|
|
479
483
|
labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
|
|
480
|
-
agent: Agent,
|
|
484
|
+
agent: Agent$1,
|
|
481
485
|
environment: RunEnvironment,
|
|
482
486
|
budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
|
|
483
487
|
one_shot: Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes." }),
|
|
@@ -530,6 +534,10 @@ var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: S
|
|
|
530
534
|
httpApiStatus: 404,
|
|
531
535
|
description: "No run with this id exists in your organization."
|
|
532
536
|
}) {};
|
|
537
|
+
var ScreenshotNotFound = class extends Schema.TaggedError()("ScreenshotNotFound", { message: Schema.String }, {
|
|
538
|
+
httpApiStatus: 404,
|
|
539
|
+
description: "This event has no screenshot."
|
|
540
|
+
}) {};
|
|
533
541
|
var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
|
|
534
542
|
httpApiStatus: 422,
|
|
535
543
|
description: "A `connection_id` in `environment.mcp_servers` does not exist or is not `active`."
|
|
@@ -557,13 +565,13 @@ var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Sch
|
|
|
557
565
|
httpApiStatus: 409,
|
|
558
566
|
description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
|
|
559
567
|
}) {};
|
|
560
|
-
const Count = Schema.Natural;
|
|
568
|
+
const Count$1 = Schema.Natural;
|
|
561
569
|
const RunUsage = Schema.Struct({
|
|
562
570
|
cost_usd: Schema.Number.annotate({ description: "Total cost of the run so far, in US dollars." }),
|
|
563
571
|
models: Schema.Array(Schema.Struct({
|
|
564
572
|
provider: Schema.String.annotate({ description: "Company serving the model, such as `anthropic`." }),
|
|
565
573
|
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." }),
|
|
574
|
+
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
575
|
cost_usd: Schema.Number.annotate({ description: "Cost of this model's usage, in US dollars." })
|
|
568
576
|
})).annotate({ description: "Model usage, one entry per model." }),
|
|
569
577
|
sandbox: Schema.Struct({
|
|
@@ -578,18 +586,23 @@ const Frame = Schema.Struct({
|
|
|
578
586
|
run_id: RunId,
|
|
579
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." }),
|
|
580
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." }),
|
|
581
590
|
captured_at: Schema.DateTimeUtc.annotate({ description: "When the line was captured." }),
|
|
582
591
|
channel: Schema.Literals(["stdout", "stderr"]).annotate({ description: "Output stream the line came from." }),
|
|
583
592
|
body: Schema.String.annotate({ description: "The line, exactly as the agent printed it." })
|
|
584
593
|
}).annotate({
|
|
585
594
|
identifier: "Frame",
|
|
586
|
-
description: "One line of the agent
|
|
595
|
+
description: "One line of output from the agent or its browser."
|
|
587
596
|
});
|
|
588
597
|
const Outcome = Schema.Literals(["succeeded", "failed"]).annotate({
|
|
589
598
|
identifier: "Outcome",
|
|
590
599
|
description: "Whether the tool call or subagent succeeded."
|
|
591
600
|
});
|
|
592
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
|
+
});
|
|
593
606
|
const eventFields = {
|
|
594
607
|
run_id: RunId,
|
|
595
608
|
seq: Schema.Int.annotate({ description: "Position of the event in the run, starting at 1. Always increasing, but some numbers may be skipped." }),
|
|
@@ -639,7 +652,8 @@ const Event = Schema.Union([
|
|
|
639
652
|
tool_call_id: ToolCallId,
|
|
640
653
|
outcome: Outcome,
|
|
641
654
|
output: Schema.String.annotate({ description: "What the tool returned, possibly truncated." }),
|
|
642
|
-
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." }))
|
|
643
657
|
}).annotate({
|
|
644
658
|
identifier: "ToolEndedEvent",
|
|
645
659
|
description: "A tool call ended; its `tool_call_id` is the one of the `tool_started` event."
|
|
@@ -660,6 +674,27 @@ const Event = Schema.Union([
|
|
|
660
674
|
identifier: "SubagentEndedEvent",
|
|
661
675
|
description: "A subagent ended."
|
|
662
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
|
+
}),
|
|
663
698
|
Schema.Struct({
|
|
664
699
|
...eventFields,
|
|
665
700
|
type: Schema.Literal("stop"),
|
|
@@ -788,6 +823,16 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
788
823
|
}).annotateMerge(OpenApi.annotations({
|
|
789
824
|
summary: "List a run's events",
|
|
790
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`."
|
|
791
836
|
})), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
|
|
792
837
|
params: { id: RunId },
|
|
793
838
|
query: { after: OptionalQueryParam(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), "Send only lines whose `seq` is greater than this.") },
|
|
@@ -808,8 +853,329 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
808
853
|
title: "Runs",
|
|
809
854
|
description: "Start coding agents and follow their work."
|
|
810
855
|
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
811
|
-
|
|
812
|
-
|
|
856
|
+
// ../contract/src/experiments.ts
|
|
857
|
+
const ExperimentId = Schema.String.pipe(Schema.brand("ExperimentId")).annotate({
|
|
858
|
+
identifier: "ExperimentId",
|
|
859
|
+
description: "Unique identifier of the experiment."
|
|
860
|
+
});
|
|
861
|
+
const ExecutionId = Schema.String.pipe(Schema.brand("ExecutionId")).annotate({
|
|
862
|
+
identifier: "ExecutionId",
|
|
863
|
+
description: "Unique identifier of the execution."
|
|
864
|
+
});
|
|
865
|
+
const Agent = Schema.Struct({
|
|
866
|
+
harness: Harness,
|
|
867
|
+
model: Schema.String.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`." })
|
|
868
|
+
}).annotate({
|
|
869
|
+
identifier: "ExperimentAgent",
|
|
870
|
+
description: "A coding agent and the model it uses."
|
|
871
|
+
});
|
|
872
|
+
const MAX_ID_LENGTH = 128;
|
|
873
|
+
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.` });
|
|
874
|
+
const withUniqueIds = (item, items) => Schema.Array(item).check(Schema.makeFilter((all) => {
|
|
875
|
+
const ids = all.map((each) => each.id);
|
|
876
|
+
return new Set(ids).size === ids.length || `Expected each of the ${items}' ids once`;
|
|
877
|
+
}));
|
|
878
|
+
const libraryIds = (items) => Schema.Array(Schema.String).annotate({ description: `Ids of ${items} from your library, as listed by \`GET /library\`.` });
|
|
879
|
+
const Evaluation = Schema.Struct({
|
|
880
|
+
id: idChosenFor("evaluation, unique in its scenario", "tests-pass"),
|
|
881
|
+
name: Schema.String.annotate({ description: "Short name of what is checked." }),
|
|
882
|
+
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?`." }),
|
|
883
|
+
weight: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "How much it counts in the run's score. Defaults to 1." })
|
|
884
|
+
}).annotate({
|
|
885
|
+
identifier: "Evaluation",
|
|
886
|
+
description: "One criterion every run of the scenario is judged on."
|
|
887
|
+
});
|
|
888
|
+
const MAX_INPUT_FILES = 20;
|
|
889
|
+
const Scenario = Schema.Struct({
|
|
890
|
+
id: idChosenFor("scenario, unique in the experiment", "checkout"),
|
|
891
|
+
name: Schema.String.annotate({ description: "Short name of the scenario." }),
|
|
892
|
+
prompt: Schema.String.annotate({ description: "What the agent is asked to do." }),
|
|
893
|
+
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.` }),
|
|
894
|
+
evaluations: withUniqueIds(Evaluation, "evaluations").annotate({ description: "What a run is judged on. Runs of a scenario without evaluations are not judged." }),
|
|
895
|
+
passing_score: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
896
|
+
minimum: 0,
|
|
897
|
+
maximum: 100
|
|
898
|
+
})).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." }))
|
|
899
|
+
}).annotate({
|
|
900
|
+
identifier: "Scenario",
|
|
901
|
+
description: "One task given to the agents."
|
|
902
|
+
});
|
|
903
|
+
const AgentTools = Schema.Struct({
|
|
904
|
+
id: idChosenFor("agent tool configuration, unique in the experiment", "with-docs-mcp"),
|
|
905
|
+
name: Schema.String.annotate({ description: "Short name of the agent tools, such as `No agent tools` for the baseline." }),
|
|
906
|
+
skills: libraryIds("skills"),
|
|
907
|
+
mcp_servers: libraryIds("MCP servers"),
|
|
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." })
|
|
910
|
+
}).annotate({
|
|
911
|
+
identifier: "AgentTools",
|
|
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."
|
|
913
|
+
});
|
|
914
|
+
const ExcludedVariant = Schema.Struct({
|
|
915
|
+
repository_id: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Id of one of the experiment's repositories. Omit it for no repository." })),
|
|
916
|
+
agent_tools_id: Schema.String.annotate({ description: "Id of one of the experiment's agent tool configurations." })
|
|
917
|
+
}).annotate({
|
|
918
|
+
identifier: "ExcludedVariant",
|
|
919
|
+
description: "A combination of repository and agent tools not to run."
|
|
920
|
+
});
|
|
921
|
+
const RunSettings = Schema.Struct({
|
|
922
|
+
repetitions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Identical runs started for each scenario, variant and agent, to compare results beyond chance." }),
|
|
923
|
+
time_limit_seconds: Schema.Finite.check(Schema.isBetween({
|
|
924
|
+
minimum: 0,
|
|
925
|
+
exclusiveMinimum: true,
|
|
926
|
+
maximum: MAX_TIME_LIMIT_SECONDS
|
|
927
|
+
})).annotate({ description: `Longest each run may last, in seconds, above 0 and at most ${MAX_TIME_LIMIT_SECONDS}.` }),
|
|
928
|
+
budget_usd: Schema.Finite.check(Schema.isBetween({
|
|
929
|
+
minimum: 0,
|
|
930
|
+
exclusiveMinimum: true,
|
|
931
|
+
maximum: MAX_BUDGET_USD
|
|
932
|
+
})).annotate({ description: `Most each run may spend, in US dollars, above 0 and at most ${MAX_BUDGET_USD}.` }),
|
|
933
|
+
setup: script("Shell script run in each run's sandbox after checkout and before the prompt."),
|
|
934
|
+
teardown: script("Shell script run in each run's sandbox after the run ends."),
|
|
935
|
+
variables: Schema.Array(Schema.Struct({
|
|
936
|
+
name: Schema.String,
|
|
937
|
+
value: Schema.String
|
|
938
|
+
})).annotate({ description: "Environment variables set in every process of each run's agent, each name once. Defaults to none." })
|
|
939
|
+
}).annotate({
|
|
940
|
+
identifier: "RunSettings",
|
|
941
|
+
description: "What every run of the experiment shares."
|
|
942
|
+
});
|
|
943
|
+
const experimentFields = {
|
|
944
|
+
name: Schema.String.annotate({ description: "Name of the experiment." }),
|
|
945
|
+
scenarios: withUniqueIds(Scenario, "scenarios"),
|
|
946
|
+
agents: Schema.Array(Agent).annotate({ description: "Agents every scenario is given to, each once." }),
|
|
947
|
+
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." }),
|
|
948
|
+
agent_tools: withUniqueIds(AgentTools, "agent_tools").annotate({ description: "Agent tool configurations each tried in turn with every repository." }),
|
|
949
|
+
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." }),
|
|
950
|
+
run_settings: RunSettings
|
|
951
|
+
};
|
|
952
|
+
const Experiment = Schema.Struct({
|
|
953
|
+
id: ExperimentId,
|
|
954
|
+
...experimentFields
|
|
955
|
+
}).annotate({
|
|
956
|
+
identifier: "Experiment",
|
|
957
|
+
description: "Scenarios given to several agents in several variants, each run judged on the scenario's evaluations."
|
|
958
|
+
});
|
|
959
|
+
const EvaluationInput = Evaluation.mapFields(Struct.evolve({ weight: (weight) => weight.pipe(Schema.withDecodingDefaultKey(Effect.succeed(1))) })).annotate({ identifier: "EvaluationInput" });
|
|
960
|
+
const ScenarioInput = Scenario.mapFields(Struct.evolve({
|
|
961
|
+
input_files: (inputFiles) => inputFiles.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
962
|
+
evaluations: () => withUniqueIds(EvaluationInput, "evaluations")
|
|
963
|
+
})).annotate({ identifier: "ScenarioInput" });
|
|
964
|
+
const RunSettingsInput = RunSettings.mapFields(Struct.evolve({ variables: (variables) => variables.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))) })).annotate({ identifier: "RunSettingsInput" });
|
|
965
|
+
const unknownExclusions = (input) => input.excluded_variants.flatMap((excluded, index) => [...input.agent_tools.some((agentTools) => agentTools.id === excluded.agent_tools_id) ? [] : [{
|
|
966
|
+
path: [
|
|
967
|
+
"excluded_variants",
|
|
968
|
+
index,
|
|
969
|
+
"agent_tools_id"
|
|
970
|
+
],
|
|
971
|
+
issue: "Expected the id of one of the agent tool configurations"
|
|
972
|
+
}], ...Option.match(excluded.repository_id, {
|
|
973
|
+
onNone: () => true,
|
|
974
|
+
onSome: (repository) => input.repositories.includes(repository)
|
|
975
|
+
}) ? [] : [{
|
|
976
|
+
path: [
|
|
977
|
+
"excluded_variants",
|
|
978
|
+
index,
|
|
979
|
+
"repository_id"
|
|
980
|
+
],
|
|
981
|
+
issue: "Expected one of the repositories"
|
|
982
|
+
}]]);
|
|
983
|
+
const ExperimentInput = Schema.Struct(experimentFields).mapFields(Struct.evolve({
|
|
984
|
+
scenarios: () => withUniqueIds(ScenarioInput, "scenarios"),
|
|
985
|
+
repositories: (repositories) => repositories.check(Schema.isUnique()).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
986
|
+
excluded_variants: (excluded) => excluded.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
987
|
+
run_settings: () => RunSettingsInput
|
|
988
|
+
})).check(Schema.makeFilter(unknownExclusions)).annotate({ identifier: "ExperimentInput" });
|
|
989
|
+
const Count = Schema.Natural;
|
|
990
|
+
const Execution = Schema.Struct({
|
|
991
|
+
id: ExecutionId,
|
|
992
|
+
experiment_id: ExperimentId,
|
|
993
|
+
number: Schema.Int.annotate({ description: "Its place among the experiment's executions, from 1." }),
|
|
994
|
+
launched_at: Schema.DateTimeUtc.annotate({ description: "When the execution was launched." }),
|
|
995
|
+
canceled_at: Schema.OptionFromOptionalKey(Schema.DateTimeUtc.annotate({ description: "When the execution was canceled. Absent unless it was." })),
|
|
996
|
+
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." }),
|
|
997
|
+
slots: Schema.Struct({
|
|
998
|
+
total: Count.annotate({ description: "Runs the execution plans." }),
|
|
999
|
+
running: Count.annotate({ description: "Runs not started yet or not ended." }),
|
|
1000
|
+
judging: Count.annotate({ description: "Ended runs waiting for their evaluation results." }),
|
|
1001
|
+
judged: Count.annotate({ description: "Runs with a verdict." }),
|
|
1002
|
+
passed: Count.annotate({ description: "Runs that passed their scenario." })
|
|
1003
|
+
}).annotate({ description: "How many of its slots, one per scenario, variant, agent and repetition, stand where." }),
|
|
1004
|
+
pass_rate: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
1005
|
+
minimum: 0,
|
|
1006
|
+
maximum: 100
|
|
1007
|
+
})).annotate({ description: "Runs that passed out of those with a verdict, as a whole percent. Absent until one run has a verdict." })),
|
|
1008
|
+
cost_usd: Schema.Number.annotate({ description: "What its runs cost so far, in US dollars." })
|
|
1009
|
+
}).annotate({
|
|
1010
|
+
identifier: "Execution",
|
|
1011
|
+
description: "One launch of an experiment: a run for every scenario, variant, agent and repetition, with the experiment as it was when launched."
|
|
1012
|
+
});
|
|
1013
|
+
const NotEvaluatedReason = Schema.Literals([
|
|
1014
|
+
"never_started",
|
|
1015
|
+
"no_evaluations",
|
|
1016
|
+
"run_not_completed",
|
|
1017
|
+
"agent_failed",
|
|
1018
|
+
"evidence_too_long",
|
|
1019
|
+
"too_many_attempts"
|
|
1020
|
+
]).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." });
|
|
1021
|
+
const Slot = Schema.Struct({
|
|
1022
|
+
id: Schema.String.annotate({ description: "Unique identifier of the slot." }),
|
|
1023
|
+
scenario_id: Schema.String,
|
|
1024
|
+
repository_id: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Absent when the run has no repository." })),
|
|
1025
|
+
agent_tools_id: Schema.String,
|
|
1026
|
+
agent: Agent,
|
|
1027
|
+
repetition: Schema.Int.annotate({ description: "Which of the identical runs it is, from 1." }),
|
|
1028
|
+
run_id: Schema.OptionFromOptionalKey(RunId.annotate({ description: "The run, once started." })),
|
|
1029
|
+
outcome: Schema.Literals([
|
|
1030
|
+
"running",
|
|
1031
|
+
"judging",
|
|
1032
|
+
"judged",
|
|
1033
|
+
"not_evaluated"
|
|
1034
|
+
]).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." }),
|
|
1035
|
+
verdict: Schema.OptionFromOptionalKey(Schema.Literals(["passed", "failed"]).annotate({ description: "Whether the run passed its scenario, once judged." })),
|
|
1036
|
+
score: Schema.OptionFromOptionalKey(Schema.Number.annotate({ description: "Weighted share of evaluations passed, out of 100, once every evaluation result is in." })),
|
|
1037
|
+
not_evaluated_reason: Schema.OptionFromOptionalKey(NotEvaluatedReason),
|
|
1038
|
+
refusal: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Why its run could not start." })),
|
|
1039
|
+
evaluation_results: Schema.Array(Schema.Struct({
|
|
1040
|
+
evaluation_id: Schema.String,
|
|
1041
|
+
passed: Schema.Boolean
|
|
1042
|
+
})).annotate({ description: "Each evaluation judged so far, and whether the run passed it." })
|
|
1043
|
+
}).annotate({
|
|
1044
|
+
identifier: "Slot",
|
|
1045
|
+
description: "One run an execution plans, for one scenario, variant, agent and repetition, and how it was judged."
|
|
1046
|
+
});
|
|
1047
|
+
var ExperimentNotFound = class extends Schema.TaggedError()("ExperimentNotFound", { message: Schema.String }, {
|
|
1048
|
+
httpApiStatus: 404,
|
|
1049
|
+
description: "No experiment with this id exists in your organization."
|
|
1050
|
+
}) {};
|
|
1051
|
+
var ExecutionNotFound = class extends Schema.TaggedError()("ExecutionNotFound", { message: Schema.String }, {
|
|
1052
|
+
httpApiStatus: 404,
|
|
1053
|
+
description: "No execution with this id exists in your organization."
|
|
1054
|
+
}) {};
|
|
1055
|
+
var ExperimentIncomplete = class extends Schema.TaggedError()("ExperimentIncomplete", {
|
|
1056
|
+
message: Schema.String,
|
|
1057
|
+
missing: Schema.Array(Schema.String).annotate({ description: "Each thing to fix before launching, in a sentence." })
|
|
1058
|
+
}, {
|
|
1059
|
+
httpApiStatus: 422,
|
|
1060
|
+
description: "The experiment cannot be launched as it is: `missing` says what to fix."
|
|
1061
|
+
}) {};
|
|
1062
|
+
const experimentParams = { id: ExperimentId };
|
|
1063
|
+
var ExperimentsApi = class extends HttpApiGroup.make("experiments").add(HttpApiEndpoint.post("create", "/experiments", {
|
|
1064
|
+
payload: ExperimentInput,
|
|
1065
|
+
success: Experiment.pipe(HttpApiSchema.status(201)),
|
|
1066
|
+
error: FileUnavailable
|
|
1067
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1068
|
+
summary: "Create an experiment",
|
|
1069
|
+
description: "Creates an experiment from its scenarios, agents, variants and run settings. Launch it to start its runs."
|
|
1070
|
+
})), HttpApiEndpoint.get("list", "/experiments", { success: List(Experiment) }).annotateMerge(OpenApi.annotations({
|
|
1071
|
+
summary: "List experiments",
|
|
1072
|
+
description: "Returns every experiment."
|
|
1073
|
+
})), HttpApiEndpoint.get("get", "/experiments/:id", {
|
|
1074
|
+
params: experimentParams,
|
|
1075
|
+
success: Experiment,
|
|
1076
|
+
error: ExperimentNotFound
|
|
1077
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1078
|
+
summary: "Get an experiment",
|
|
1079
|
+
description: "Returns an experiment."
|
|
1080
|
+
})), HttpApiEndpoint.put("update", "/experiments/:id", {
|
|
1081
|
+
params: experimentParams,
|
|
1082
|
+
payload: ExperimentInput,
|
|
1083
|
+
success: Experiment,
|
|
1084
|
+
error: [ExperimentNotFound, FileUnavailable]
|
|
1085
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1086
|
+
summary: "Update an experiment",
|
|
1087
|
+
description: "Replaces the whole experiment: send it complete, as `GET /experiments/{id}` returns it. Executions already launched keep the experiment as it was."
|
|
1088
|
+
})), HttpApiEndpoint.delete("delete", "/experiments/:id", {
|
|
1089
|
+
params: experimentParams,
|
|
1090
|
+
success: HttpApiSchema.NoContent,
|
|
1091
|
+
error: ExperimentNotFound
|
|
1092
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1093
|
+
summary: "Delete an experiment",
|
|
1094
|
+
description: "Deletes an experiment. Its executions, their runs and results stay."
|
|
1095
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1096
|
+
title: "Experiments",
|
|
1097
|
+
description: "Define scenarios to give several agents in several variants, judged on what they did."
|
|
1098
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1099
|
+
const executionParams = { id: ExecutionId };
|
|
1100
|
+
var ExecutionsApi = class extends HttpApiGroup.make("executions").add(HttpApiEndpoint.post("launch", "/experiments/:id/executions", {
|
|
1101
|
+
params: experimentParams,
|
|
1102
|
+
success: Execution.pipe(HttpApiSchema.status(201)),
|
|
1103
|
+
error: [
|
|
1104
|
+
ExperimentNotFound,
|
|
1105
|
+
ExperimentIncomplete,
|
|
1106
|
+
InsufficientBalance,
|
|
1107
|
+
OrganizationBlocked
|
|
1108
|
+
]
|
|
1109
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1110
|
+
summary: "Launch an experiment",
|
|
1111
|
+
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."
|
|
1112
|
+
})), HttpApiEndpoint.get("list", "/experiments/:id/executions", {
|
|
1113
|
+
params: experimentParams,
|
|
1114
|
+
success: List(Execution)
|
|
1115
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1116
|
+
summary: "List an experiment's executions",
|
|
1117
|
+
description: "Returns the experiment's executions, the latest launched first, even after it was deleted."
|
|
1118
|
+
})), HttpApiEndpoint.get("get", "/executions/:id", {
|
|
1119
|
+
params: executionParams,
|
|
1120
|
+
success: Execution,
|
|
1121
|
+
error: ExecutionNotFound
|
|
1122
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1123
|
+
summary: "Get an execution",
|
|
1124
|
+
description: "Returns an execution, where its runs stand and its pass rate. Poll it until it is no longer `pending`."
|
|
1125
|
+
})), HttpApiEndpoint.get("listSlots", "/executions/:id/slots", {
|
|
1126
|
+
params: executionParams,
|
|
1127
|
+
success: List(Slot),
|
|
1128
|
+
error: ExecutionNotFound
|
|
1129
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1130
|
+
summary: "List an execution's slots",
|
|
1131
|
+
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."
|
|
1132
|
+
})), HttpApiEndpoint.post("cancel", "/executions/:id/cancel", {
|
|
1133
|
+
params: executionParams,
|
|
1134
|
+
success: Execution.pipe(HttpApiSchema.status(202)),
|
|
1135
|
+
error: ExecutionNotFound
|
|
1136
|
+
}).annotateMerge(OpenApi.annotations({
|
|
1137
|
+
summary: "Cancel an execution",
|
|
1138
|
+
description: "Starts none of its runs left and cancels those started. Canceling an execution already canceled does nothing."
|
|
1139
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1140
|
+
title: "Executions",
|
|
1141
|
+
description: "Launch experiments and read their results."
|
|
1142
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1143
|
+
// ../contract/src/library.ts
|
|
1144
|
+
const id = Schema.String.annotate({ description: "Id to pick it in an experiment." });
|
|
1145
|
+
const Library = Schema.Struct({
|
|
1146
|
+
repositories: Schema.Array(Schema.Struct({
|
|
1147
|
+
id,
|
|
1148
|
+
name: Schema.String,
|
|
1149
|
+
url: Schema.String.annotate({ description: "URL of the Git repository." }),
|
|
1150
|
+
ref: Schema.String.annotate({ description: "Branch, tag or commit checked out." })
|
|
1151
|
+
})),
|
|
1152
|
+
skills: Schema.Array(Schema.Struct({
|
|
1153
|
+
id,
|
|
1154
|
+
name: Schema.String,
|
|
1155
|
+
description: Schema.String
|
|
1156
|
+
})),
|
|
1157
|
+
mcp_servers: Schema.Array(Schema.Struct({
|
|
1158
|
+
id,
|
|
1159
|
+
name: Schema.String.annotate({ description: "Name the agent knows the MCP server by." }),
|
|
1160
|
+
url: Schema.String,
|
|
1161
|
+
connected: Schema.Boolean.annotate({ description: "Whether runs can use it: always for one without an account, otherwise once connected in the web app." })
|
|
1162
|
+
})),
|
|
1163
|
+
command_line_tools: Schema.Array(Schema.Struct({
|
|
1164
|
+
id,
|
|
1165
|
+
name: Schema.String
|
|
1166
|
+
}))
|
|
1167
|
+
}).annotate({
|
|
1168
|
+
identifier: "Library",
|
|
1169
|
+
description: "What your organization keeps for its experiments, added in the web app."
|
|
1170
|
+
});
|
|
1171
|
+
var LibraryApi = class extends HttpApiGroup.make("library").add(HttpApiEndpoint.get("get", "/library", { success: Library }).annotateMerge(OpenApi.annotations({
|
|
1172
|
+
summary: "Get the library",
|
|
1173
|
+
description: "Returns the repositories, skills, MCP servers and command-line tools your experiments can pick, by id."
|
|
1174
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
1175
|
+
title: "Library",
|
|
1176
|
+
description: "What your experiments can give their agents."
|
|
1177
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
1178
|
+
// ../contract/src/system.ts
|
|
813
1179
|
const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
|
|
814
1180
|
var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
|
|
815
1181
|
summary: "Check health",
|
|
@@ -818,23 +1184,23 @@ var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).ad
|
|
|
818
1184
|
summary: "List egress IPs",
|
|
819
1185
|
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
1186
|
}))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(FilesApi).add(ConnectionsApi).add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
|
|
1187
|
+
// ../contract/src/api.ts
|
|
1188
|
+
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
1189
|
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."
|
|
1190
|
+
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
1191
|
})) {};
|
|
827
|
-
|
|
828
|
-
//#region ../contract/src/client.ts
|
|
1192
|
+
// ../contract/src/client.ts
|
|
829
1193
|
const CLI_USER_AGENT = "tracks";
|
|
830
1194
|
var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
|
|
831
1195
|
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
|
|
832
1196
|
const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
|
|
833
1197
|
return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
|
|
834
|
-
}) })).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
|
+
})))));
|
|
835
1202
|
};
|
|
836
|
-
|
|
837
|
-
//#region ../env/src/env.ts
|
|
1203
|
+
// ../env/src/env.ts
|
|
838
1204
|
const Secret = Schema.Redacted(Schema.NonEmptyString);
|
|
839
1205
|
const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
|
|
840
1206
|
const Port = Schema.Int.check(Schema.isBetween({
|
|
@@ -846,7 +1212,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
|
846
1212
|
"127.0.0.1",
|
|
847
1213
|
"[::1]"
|
|
848
1214
|
]);
|
|
849
|
-
const
|
|
1215
|
+
const TRACKS_PUBLIC_API_URL = `https://tracks.alpic.ai/api`;
|
|
850
1216
|
const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
|
|
851
1217
|
const ENCRYPTION_KEY_BYTES = 32;
|
|
852
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}`)));
|
|
@@ -871,16 +1237,18 @@ const Secrets = Schema.Struct({
|
|
|
871
1237
|
TYPESAFE_API_KEY: Secret,
|
|
872
1238
|
DATABASE_PASSWORD: Secret,
|
|
873
1239
|
CONNECTION_ENCRYPTION_KEY: EncryptionKey,
|
|
1240
|
+
TRAFFIC_ENCRYPTION_KEY: EncryptionKey,
|
|
874
1241
|
STRIPE_SECRET_KEY: StripeKey("rk_"),
|
|
875
1242
|
STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
|
|
876
1243
|
IDENTITY_PROVIDER_CLIENT_ID: Secret,
|
|
877
1244
|
IDENTITY_PROVIDER_CLIENT_SECRET: Secret,
|
|
878
1245
|
SESSION_SECRET: Secret,
|
|
1246
|
+
VERCEL_CLIENT_SECRET: Secret,
|
|
879
1247
|
SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
|
|
880
1248
|
});
|
|
881
1249
|
const Variables = Schema.Struct({
|
|
882
1250
|
PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
|
|
883
|
-
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))),
|
|
884
1252
|
DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
|
|
885
1253
|
AGENT_RUNNER_URL: SecureUrl,
|
|
886
1254
|
FILES_BUCKET: Schema.NonEmptyString,
|
|
@@ -904,7 +1272,12 @@ const Variables = Schema.Struct({
|
|
|
904
1272
|
DEPLOYMENT_ENVIRONMENT: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("local"))),
|
|
905
1273
|
OTLP_METRICS_URL: Schema.optionalKey(Schema.URLFromString),
|
|
906
1274
|
RELEASE: Schema.optionalKey(Schema.NonEmptyString),
|
|
907
|
-
|
|
1275
|
+
POSTHOG_KEY: Schema.optionalKey(Schema.NonEmptyString),
|
|
1276
|
+
POSTHOG_HOST: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://us.i.posthog.com"))),
|
|
1277
|
+
CLOUDFLARE_CLIENT_ID: Schema.NonEmptyString,
|
|
1278
|
+
VERCEL_CLIENT_ID: Schema.NonEmptyString,
|
|
1279
|
+
VERCEL_INTEGRATION_SLUG: Schema.NonEmptyString,
|
|
1280
|
+
IDENTITY_PROVIDER_URL: SecureUrl.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://idp.alpic.ai")))
|
|
908
1281
|
});
|
|
909
1282
|
const Env = Schema.Struct({
|
|
910
1283
|
...Secrets.fields,
|
|
@@ -940,6 +1313,13 @@ const IDENTITY_ENV = [
|
|
|
940
1313
|
"IDENTITY_PROVIDER_CLIENT_SECRET",
|
|
941
1314
|
"SESSION_SECRET"
|
|
942
1315
|
];
|
|
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
|
+
];
|
|
943
1323
|
const AGENT_RUNNER_ENV = [
|
|
944
1324
|
"FILES_BUCKET",
|
|
945
1325
|
"FILES_ENDPOINT",
|
|
@@ -952,6 +1332,8 @@ const AGENT_RUNNER_ENV = [
|
|
|
952
1332
|
const LOG_ENV = ["LOG_FORMAT", "LOG_LEVEL"];
|
|
953
1333
|
const OBSERVABILITY_ENV = [
|
|
954
1334
|
"SENTRY_DSN",
|
|
1335
|
+
"POSTHOG_KEY",
|
|
1336
|
+
"POSTHOG_HOST",
|
|
955
1337
|
"DEPLOYMENT_ENVIRONMENT",
|
|
956
1338
|
"OTLP_METRICS_URL",
|
|
957
1339
|
"RELEASE"
|
|
@@ -964,17 +1346,15 @@ const SERVICE_ENV = [
|
|
|
964
1346
|
...OBSERVABILITY_ENV,
|
|
965
1347
|
...AGENT_RUNNER_ENV,
|
|
966
1348
|
...PAYMENTS_ENV,
|
|
967
|
-
...IDENTITY_ENV
|
|
1349
|
+
...IDENTITY_ENV,
|
|
1350
|
+
...TRAFFIC_ENV
|
|
968
1351
|
];
|
|
969
|
-
const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
|
|
970
1352
|
const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
|
|
971
1353
|
SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
|
|
972
1354
|
const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
//#endregion
|
|
977
|
-
//#region src/connections.ts
|
|
1355
|
+
// package.json
|
|
1356
|
+
var version = "0.9.1";
|
|
1357
|
+
// src/connections.ts
|
|
978
1358
|
const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
|
|
979
1359
|
const encodeConnections = Schema.encodeEffect(Schema.fromJsonString(List(Connection), { space: 2 }));
|
|
980
1360
|
const connectionId = Argument.String("id").pipe(Argument.withDescription("Connection id"), Argument.withSchema(ConnectionId));
|
|
@@ -1037,7 +1417,7 @@ const confirmationOf = (params, id) => {
|
|
|
1037
1417
|
onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
|
|
1038
1418
|
});
|
|
1039
1419
|
};
|
|
1040
|
-
const create$
|
|
1420
|
+
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
1421
|
const client = yield* ApiClient;
|
|
1042
1422
|
const callback = yield* loopbackCallback;
|
|
1043
1423
|
const created = yield* client.connections.create({ payload: {
|
|
@@ -1057,11 +1437,11 @@ const create$1 = Command.make("create", { server: Argument.String("server-url").
|
|
|
1057
1437
|
});
|
|
1058
1438
|
yield* Console.log(yield* encodeConnection(connection));
|
|
1059
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"));
|
|
1060
|
-
const list = Command.make("list", {}, Effect.fn("connections.list")(function* () {
|
|
1440
|
+
const list$2 = Command.make("list", {}, Effect.fn("connections.list")(function* () {
|
|
1061
1441
|
const client = yield* ApiClient;
|
|
1062
1442
|
yield* Console.log(yield* encodeConnections(yield* client.connections.list()));
|
|
1063
1443
|
})).pipe(Command.withDescription("List connections"));
|
|
1064
|
-
const get$
|
|
1444
|
+
const get$2 = Command.make("get", { id: connectionId }, Effect.fn("connections.get")(function* ({ id }) {
|
|
1065
1445
|
const client = yield* ApiClient;
|
|
1066
1446
|
yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
|
|
1067
1447
|
})).pipe(Command.withDescription("Show a connection"));
|
|
@@ -1069,13 +1449,150 @@ const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn(
|
|
|
1069
1449
|
yield* (yield* ApiClient).connections.delete({ params: { id } });
|
|
1070
1450
|
})).pipe(Command.withDescription("Delete a connection and the access it holds"));
|
|
1071
1451
|
const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
|
|
1452
|
+
create$2,
|
|
1453
|
+
list$2,
|
|
1454
|
+
get$2,
|
|
1455
|
+
deleteConnection
|
|
1456
|
+
]));
|
|
1457
|
+
// src/experiments.ts
|
|
1458
|
+
const encodeExperiment = Schema.encodeEffect(Schema.fromJsonString(Experiment, { space: 2 }));
|
|
1459
|
+
const encodeExperiments = Schema.encodeEffect(Schema.fromJsonString(List(Experiment), { space: 2 }));
|
|
1460
|
+
const encodeExecution = Schema.encodeEffect(Schema.fromJsonString(Execution, { space: 2 }));
|
|
1461
|
+
const encodeExecutions = Schema.encodeEffect(Schema.fromJsonString(List(Execution), { space: 2 }));
|
|
1462
|
+
const encodeSlots = Schema.encodeEffect(Schema.fromJsonString(List(Slot), { space: 2 }));
|
|
1463
|
+
const experimentId = Argument.String("id").pipe(Argument.withDescription("Experiment id"), Argument.withSchema(ExperimentId));
|
|
1464
|
+
const executionId = Argument.String("id").pipe(Argument.withDescription("Execution id"), Argument.withSchema(ExecutionId));
|
|
1465
|
+
const definition = Argument.FileSchema("file", ExperimentInput).pipe(Argument.withDescription("JSON file of the experiment, as `tracks experiments get` prints it, without its id"));
|
|
1466
|
+
const POLL_INTERVAL = "10 seconds";
|
|
1467
|
+
const POLL_RETRIES = {
|
|
1468
|
+
schedule: Schedule.exponential("2 seconds").pipe(Schedule.jittered),
|
|
1469
|
+
times: 5
|
|
1470
|
+
};
|
|
1471
|
+
var PassRateTooLow = class extends Schema.TaggedError()("PassRateTooLow", {
|
|
1472
|
+
passRate: Schema.Option(Schema.Number),
|
|
1473
|
+
minimum: Schema.Number
|
|
1474
|
+
}) {
|
|
1475
|
+
get message() {
|
|
1476
|
+
const reached = Option.match(this.passRate, {
|
|
1477
|
+
onNone: () => "no run was judged",
|
|
1478
|
+
onSome: (rate) => `${rate}%`
|
|
1479
|
+
});
|
|
1480
|
+
return `The execution's pass rate is below ${this.minimum}%: ${reached}`;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const create$1 = Command.make("create", { definition }, Effect.fn("experiments.create")(function* ({ definition: payload }) {
|
|
1484
|
+
const client = yield* ApiClient;
|
|
1485
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.create({ payload })));
|
|
1486
|
+
})).pipe(Command.withDescription("Create an experiment from a JSON file"));
|
|
1487
|
+
const list$1 = Command.make("list", {}, Effect.fn("experiments.list")(function* () {
|
|
1488
|
+
const client = yield* ApiClient;
|
|
1489
|
+
yield* Console.log(yield* encodeExperiments(yield* client.experiments.list()));
|
|
1490
|
+
})).pipe(Command.withDescription("List experiments"));
|
|
1491
|
+
const get$1 = Command.make("get", { id: experimentId }, Effect.fn("experiments.get")(function* ({ id }) {
|
|
1492
|
+
const client = yield* ApiClient;
|
|
1493
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.get({ params: { id } })));
|
|
1494
|
+
})).pipe(Command.withDescription("Show an experiment"));
|
|
1495
|
+
const update = Command.make("update", {
|
|
1496
|
+
id: experimentId,
|
|
1497
|
+
definition
|
|
1498
|
+
}, Effect.fn("experiments.update")(function* ({ id, definition: payload }) {
|
|
1499
|
+
const client = yield* ApiClient;
|
|
1500
|
+
yield* Console.log(yield* encodeExperiment(yield* client.experiments.update({
|
|
1501
|
+
params: { id },
|
|
1502
|
+
payload
|
|
1503
|
+
})));
|
|
1504
|
+
})).pipe(Command.withDescription("Replace a whole experiment with a JSON file"));
|
|
1505
|
+
const deleteExperiment = Command.make("delete", { id: experimentId }, Effect.fn("experiments.delete")(function* ({ id }) {
|
|
1506
|
+
yield* (yield* ApiClient).experiments.delete({ params: { id } });
|
|
1507
|
+
})).pipe(Command.withDescription("Delete an experiment; its executions and their results stay"));
|
|
1508
|
+
const launch = Command.make("launch", {
|
|
1509
|
+
id: experimentId,
|
|
1510
|
+
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)),
|
|
1511
|
+
minPassRate: Flag.Int("min-pass-rate").pipe(Flag.withSchema(Schema.Int.check(Schema.isBetween({
|
|
1512
|
+
minimum: 0,
|
|
1513
|
+
maximum: 100
|
|
1514
|
+
}))), Flag.withDescription("Wait, then exit with code 2 when the pass rate ends below this percentage, from 0 to 100"), Flag.optional)
|
|
1515
|
+
}, Effect.fn("experiments.launch")(function* ({ id, wait, minPassRate }) {
|
|
1516
|
+
const client = yield* ApiClient;
|
|
1517
|
+
const launched = yield* client.executions.launch({ params: { id } });
|
|
1518
|
+
if (!wait && Option.isNone(minPassRate)) return yield* Console.log(yield* encodeExecution(launched));
|
|
1519
|
+
yield* Console.error(`Launched execution ${launched.id}: ${launched.slots.total} runs`);
|
|
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({
|
|
1524
|
+
schedule: Schedule.spaced(POLL_INTERVAL),
|
|
1525
|
+
until: (execution) => !execution.pending
|
|
1526
|
+
}), Effect.tapErrorTag("HttpClientError", () => Console.error(`Execution ${launched.id} keeps running: check it with tracks executions get ${launched.id}`)));
|
|
1527
|
+
yield* Console.log(yield* encodeExecution(done));
|
|
1528
|
+
if (Option.isSome(minPassRate) && !Option.exists(done.pass_rate, (rate) => rate >= minPassRate.value)) return yield* new PassRateTooLow({
|
|
1529
|
+
passRate: done.pass_rate,
|
|
1530
|
+
minimum: minPassRate.value
|
|
1531
|
+
});
|
|
1532
|
+
})).pipe(Command.withDescription("Launch an experiment: start a run for every scenario, variant, agent and repetition"));
|
|
1533
|
+
const executionsOfExperiment = Command.make("executions", { id: experimentId }, Effect.fn("experiments.executions")(function* ({ id }) {
|
|
1534
|
+
const client = yield* ApiClient;
|
|
1535
|
+
yield* Console.log(yield* encodeExecutions(yield* client.executions.list({ params: { id } })));
|
|
1536
|
+
})).pipe(Command.withDescription("List an experiment's executions"));
|
|
1537
|
+
const experiments = Command.make("experiments").pipe(Command.withDescription("Compare agents on your scenarios"), Command.withSubcommands([
|
|
1072
1538
|
create$1,
|
|
1073
|
-
list,
|
|
1539
|
+
list$1,
|
|
1074
1540
|
get$1,
|
|
1075
|
-
|
|
1541
|
+
update,
|
|
1542
|
+
deleteExperiment,
|
|
1543
|
+
launch,
|
|
1544
|
+
executionsOfExperiment
|
|
1545
|
+
]));
|
|
1546
|
+
const getExecution = Command.make("get", { id: executionId }, Effect.fn("executions.get")(function* ({ id }) {
|
|
1547
|
+
const client = yield* ApiClient;
|
|
1548
|
+
yield* Console.log(yield* encodeExecution(yield* client.executions.get({ params: { id } })));
|
|
1549
|
+
})).pipe(Command.withDescription("Show an execution, where its runs stand and its pass rate"));
|
|
1550
|
+
const slots = Command.make("slots", { id: executionId }, Effect.fn("executions.slots")(function* ({ id }) {
|
|
1551
|
+
const client = yield* ApiClient;
|
|
1552
|
+
yield* Console.log(yield* encodeSlots(yield* client.executions.listSlots({ params: { id } })));
|
|
1553
|
+
})).pipe(Command.withDescription("List every run an execution plans, with its verdict once judged"));
|
|
1554
|
+
const cancel$1 = Command.make("cancel", { id: executionId }, Effect.fn("executions.cancel")(function* ({ id }) {
|
|
1555
|
+
const client = yield* ApiClient;
|
|
1556
|
+
yield* Console.log(yield* encodeExecution(yield* client.executions.cancel({ params: { id } })));
|
|
1557
|
+
})).pipe(Command.withDescription("Cancel an execution: start none of its runs left and cancel those started"));
|
|
1558
|
+
const executions = Command.make("executions").pipe(Command.withDescription("Follow and cancel experiments' executions"), Command.withSubcommands([
|
|
1559
|
+
getExecution,
|
|
1560
|
+
slots,
|
|
1561
|
+
cancel$1
|
|
1076
1562
|
]));
|
|
1077
|
-
|
|
1078
|
-
|
|
1563
|
+
// src/library.ts
|
|
1564
|
+
const encodeLibrary = Schema.encodeEffect(Schema.fromJsonString(Library, { space: 2 }));
|
|
1565
|
+
const library = Command.make("library", {}, Effect.fn("library")(function* () {
|
|
1566
|
+
const client = yield* ApiClient;
|
|
1567
|
+
yield* Console.log(yield* encodeLibrary(yield* client.library.get()));
|
|
1568
|
+
})).pipe(Command.withDescription("List the repositories, skills, MCP servers and command-line tools experiments can pick"));
|
|
1569
|
+
// src/files.ts
|
|
1570
|
+
const CONTENT_TYPES = {
|
|
1571
|
+
".png": "image/png",
|
|
1572
|
+
".jpg": "image/jpeg",
|
|
1573
|
+
".jpeg": "image/jpeg",
|
|
1574
|
+
".webp": "image/webp",
|
|
1575
|
+
".gif": "image/gif",
|
|
1576
|
+
".pdf": "application/pdf",
|
|
1577
|
+
".txt": "text/plain",
|
|
1578
|
+
".md": "text/markdown",
|
|
1579
|
+
".csv": "text/csv",
|
|
1580
|
+
".json": "application/json"
|
|
1581
|
+
};
|
|
1582
|
+
const uploadFile = Effect.fn("uploadFile")(function* (localPath) {
|
|
1583
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
1584
|
+
const path = yield* Path.Path;
|
|
1585
|
+
const contentType = CONTENT_TYPES[path.extname(localPath).toLowerCase()] ?? "application/octet-stream";
|
|
1586
|
+
const bytes = yield* fileSystem.readFile(localPath);
|
|
1587
|
+
const upload = yield* (yield* ApiClient).files.createUpload({ payload: {
|
|
1588
|
+
name: path.basename(localPath),
|
|
1589
|
+
content_type: contentType,
|
|
1590
|
+
size: bytes.byteLength
|
|
1591
|
+
} });
|
|
1592
|
+
yield* (yield* HttpClient.HttpClient).execute(HttpClientRequest.put(upload.url).pipe(HttpClientRequest.bodyUint8Array(bytes, contentType), HttpClientRequest.setHeaders(upload.headers))).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk));
|
|
1593
|
+
return upload.id;
|
|
1594
|
+
});
|
|
1595
|
+
// src/runs.ts
|
|
1079
1596
|
const encodeRun = Schema.encodeEffect(Schema.fromJsonString(Run, { space: 2 }));
|
|
1080
1597
|
const encodeFrames = Schema.encodeEffect(Schema.fromJsonString(List(Frame), { space: 2 }));
|
|
1081
1598
|
const encodeEvents = Schema.encodeEffect(Schema.fromJsonString(List(Event), { space: 2 }));
|
|
@@ -1120,6 +1637,7 @@ const fixedHeaderOf = (entry) => {
|
|
|
1120
1637
|
value: entry.slice(colon + 1).trim()
|
|
1121
1638
|
});
|
|
1122
1639
|
};
|
|
1640
|
+
const labelFlag = (description) => Flag.String("label").pipe(Flag.withDescription(description), Flag.atLeast(0), Flag.withSchema(LabelFilters));
|
|
1123
1641
|
var FixedHeaderUnmatched = class extends Schema.TaggedError()("FixedHeaderUnmatched", { server: Schema.String }) {
|
|
1124
1642
|
get message() {
|
|
1125
1643
|
return `--mcp-header ${this.server}=… needs an --mcp ${this.server}=URL`;
|
|
@@ -1129,6 +1647,7 @@ const create = Command.make("create", {
|
|
|
1129
1647
|
prompt: Argument.String("prompt").pipe(Argument.withDescription("Prompt sent to the agent")),
|
|
1130
1648
|
harness: Flag.Literals("harness", Harness.literals).pipe(Flag.withAlias("H"), Flag.withDescription("Harness running the agent")),
|
|
1131
1649
|
model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model used by the harness")),
|
|
1650
|
+
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
1651
|
repository: Flag.String("repository").pipe(Flag.withAlias("r"), Flag.withDescription("Git repository cloned into the sandbox"), Flag.optional),
|
|
1133
1652
|
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
1653
|
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),
|
|
@@ -1143,12 +1662,14 @@ const create = Command.make("create", {
|
|
|
1143
1662
|
})),
|
|
1144
1663
|
onSome: Effect.succeed
|
|
1145
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)),
|
|
1146
1666
|
budget: Flag.Finite("budget").pipe(Flag.withAlias("b"), Flag.withDescription("Most the run may spend, in US dollars"), Flag.optional),
|
|
1147
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),
|
|
1148
1668
|
idleTimeout: Flag.Int("idle-timeout").pipe(Flag.withDescription("Seconds an idle run waits for a follow-up before it ends"), Flag.optional),
|
|
1149
1669
|
timeLimit: Flag.Int("time-limit").pipe(Flag.withDescription("Longest the run may last, in seconds, before it is stopped"), Flag.optional),
|
|
1150
|
-
idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional)
|
|
1151
|
-
|
|
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 }) {
|
|
1152
1673
|
const unmatched = mcpHeader.find((header) => !mcp.some((server) => server.url !== void 0 && server.name === header.server));
|
|
1153
1674
|
if (unmatched !== void 0) return yield* new FixedHeaderUnmatched({ server: unmatched.server });
|
|
1154
1675
|
const mcpServers = mcp.map((server) => {
|
|
@@ -1159,6 +1680,7 @@ const create = Command.make("create", {
|
|
|
1159
1680
|
headers: Option.some(Object.fromEntries(headers.map(({ name, value }) => [name, value])))
|
|
1160
1681
|
};
|
|
1161
1682
|
});
|
|
1683
|
+
const inputFiles = yield* Effect.forEach(file, uploadFile, { concurrency: "unbounded" });
|
|
1162
1684
|
const run = yield* (yield* ApiClient).runs.create({
|
|
1163
1685
|
headers: { "idempotency-key": idempotencyKey },
|
|
1164
1686
|
payload: {
|
|
@@ -1167,22 +1689,38 @@ const create = Command.make("create", {
|
|
|
1167
1689
|
model
|
|
1168
1690
|
},
|
|
1169
1691
|
prompt,
|
|
1692
|
+
input_files: inputFiles,
|
|
1170
1693
|
environment: {
|
|
1171
1694
|
repository,
|
|
1172
1695
|
variables: Option.some(Object.fromEntries(env.map(({ name, value }) => [name, value]))),
|
|
1173
1696
|
setup,
|
|
1174
1697
|
teardown,
|
|
1175
1698
|
skills: Option.none(),
|
|
1176
|
-
mcp_servers: Option.some(mcpServers)
|
|
1699
|
+
mcp_servers: Option.some(mcpServers),
|
|
1700
|
+
browser
|
|
1177
1701
|
},
|
|
1178
1702
|
budget_usd: budget,
|
|
1179
1703
|
one_shot: oneShot,
|
|
1180
1704
|
idle_timeout_seconds: idleTimeout,
|
|
1181
|
-
time_limit_seconds: timeLimit
|
|
1705
|
+
time_limit_seconds: timeLimit,
|
|
1706
|
+
labels: label
|
|
1182
1707
|
}
|
|
1183
1708
|
});
|
|
1184
1709
|
yield* Console.log(yield* encodeRun(run));
|
|
1185
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"));
|
|
1186
1724
|
const get = Command.make("get", { id: runId }, Effect.fn("runs.get")(function* ({ id }) {
|
|
1187
1725
|
const run = yield* (yield* ApiClient).runs.get({ params: { id } });
|
|
1188
1726
|
yield* Console.log(yield* encodeRun(run));
|
|
@@ -1224,6 +1762,14 @@ const events = Command.make("events", {
|
|
|
1224
1762
|
yield* Console.log(yield* encodeEvents(page));
|
|
1225
1763
|
})).pipe(Command.withDescription("List a run's events"));
|
|
1226
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
|
+
};
|
|
1227
1773
|
const follow = Command.make("follow", {
|
|
1228
1774
|
id: runId,
|
|
1229
1775
|
after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only frames, or events with --events, with a greater seq"), Flag.optional),
|
|
@@ -1240,9 +1786,15 @@ const follow = Command.make("follow", {
|
|
|
1240
1786
|
let lastSeen = after;
|
|
1241
1787
|
while (true) {
|
|
1242
1788
|
lastSeen = Option.orElse(yield* followOnce(lastSeen), () => lastSeen);
|
|
1243
|
-
|
|
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
|
+
});
|
|
1244
1796
|
}
|
|
1245
|
-
})).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"));
|
|
1246
1798
|
const sendPrompt = Command.make("send-prompt", {
|
|
1247
1799
|
id: runId,
|
|
1248
1800
|
text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
|
|
@@ -1281,6 +1833,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
|
|
|
1281
1833
|
})).pipe(Command.withDescription("Stop a run right away, even mid-prompt"));
|
|
1282
1834
|
const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
|
|
1283
1835
|
create,
|
|
1836
|
+
list,
|
|
1284
1837
|
get,
|
|
1285
1838
|
sendPrompt,
|
|
1286
1839
|
prompts,
|
|
@@ -1291,27 +1844,36 @@ const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), C
|
|
|
1291
1844
|
end,
|
|
1292
1845
|
cancel
|
|
1293
1846
|
]));
|
|
1294
|
-
|
|
1295
|
-
//#region src/main.ts
|
|
1847
|
+
// src/tracks.ts
|
|
1296
1848
|
const health = Command.make("health", {}, Effect.fn("health")(function* () {
|
|
1297
1849
|
yield* (yield* ApiClient).health();
|
|
1298
1850
|
yield* Console.log("ok");
|
|
1299
1851
|
})).pipe(Command.withDescription("Check that the API is reachable"));
|
|
1300
|
-
const
|
|
1852
|
+
const tracks = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes and compare them on your scenarios"), Command.withSubcommands([
|
|
1301
1853
|
health,
|
|
1302
1854
|
runs,
|
|
1303
|
-
connections
|
|
1855
|
+
connections,
|
|
1856
|
+
experiments,
|
|
1857
|
+
executions,
|
|
1858
|
+
library
|
|
1304
1859
|
]));
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}))
|
|
1310
|
-
const
|
|
1311
|
-
|
|
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
|
+
}));
|
|
1873
|
+
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer).pipe(Layer.provideMerge(FetchHttpClient.layer));
|
|
1874
|
+
Command.run(tracks, { version }).pipe(Effect.catch((error) => {
|
|
1312
1875
|
const explainedByHelp = error._tag === "ShowHelp";
|
|
1313
1876
|
const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
|
|
1314
|
-
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)));
|
|
1315
1878
|
}), Effect.provide(MainLayer), NodeRuntime.runMain);
|
|
1316
|
-
//#endregion
|
|
1317
1879
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/tracks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Run coding agents in sandboxes from the command line",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -20,19 +20,22 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@effect/platform-node": "^4.0.
|
|
24
|
-
"effect": "^4.0.
|
|
23
|
+
"@effect/platform-node": "^4.0.7",
|
|
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
|
-
"rolldown": "^1.2.
|
|
31
|
+
"rolldown": "^1.2.15"
|
|
30
32
|
},
|
|
31
33
|
"engines": {
|
|
32
34
|
"node": ">=24"
|
|
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
|
}
|