karotte 3.0.4__py3-none-any.whl

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 (193) hide show
  1. karotte/LICENSE +21 -0
  2. karotte/README.md +270 -0
  3. karotte/__init__.py +53 -0
  4. karotte/agents/__init__.py +52 -0
  5. karotte/agents/agent.py +97 -0
  6. karotte/agents/backend_source.py +47 -0
  7. karotte/agents/builtin.py +13 -0
  8. karotte/agents/builtin_source.py +356 -0
  9. karotte/agents/cli_agent.py +95 -0
  10. karotte/agents/external.py +14 -0
  11. karotte/agents/fake_source.py +27 -0
  12. karotte/agents/message_loop.py +305 -0
  13. karotte/agents/message_source.py +22 -0
  14. karotte/agents/mistral_vibe/__init__.py +3 -0
  15. karotte/agents/mistral_vibe/adapter.py +180 -0
  16. karotte/agents/mistral_vibe/agent.py +283 -0
  17. karotte/agents/models.py +48 -0
  18. karotte/backend_client.py +258 -0
  19. karotte/build.py +159 -0
  20. karotte/cgroups.py +396 -0
  21. karotte/check_credentials.py +250 -0
  22. karotte/check_paths.py +32 -0
  23. karotte/cli/__init__.py +112 -0
  24. karotte/cli/agents.py +164 -0
  25. karotte/cli/build.py +50 -0
  26. karotte/cli/check.py +71 -0
  27. karotte/cli/create_env.py +83 -0
  28. karotte/cli/create_run_config.py +82 -0
  29. karotte/cli/dashboard.py +15 -0
  30. karotte/cli/models.py +51 -0
  31. karotte/cli/run.py +497 -0
  32. karotte/cli/tasks.py +48 -0
  33. karotte/cli/templates.py +44 -0
  34. karotte/cli/update.py +44 -0
  35. karotte/confinement.py +719 -0
  36. karotte/container.py +69 -0
  37. karotte/create_env.py +244 -0
  38. karotte/demoted.py +57 -0
  39. karotte/evaluation_runner.py +345 -0
  40. karotte/fake_model.py +20 -0
  41. karotte/file_quota.py +236 -0
  42. karotte/gcs.py +70 -0
  43. karotte/hardware.py +67 -0
  44. karotte/hide_run_config.py +48 -0
  45. karotte/init_shim.py +84 -0
  46. karotte/judges/__init__.py +25 -0
  47. karotte/judges/_composite_judge.py +194 -0
  48. karotte/judges/always_pass_judge.py +11 -0
  49. karotte/judges/executable_judge.py +185 -0
  50. karotte/judges/judge.py +27 -0
  51. karotte/judges/regex_judge.py +34 -0
  52. karotte/judges/rubric_context.py +130 -0
  53. karotte/judges/rubric_judge.py +162 -0
  54. karotte/load_tasks.py +45 -0
  55. karotte/log.py +12 -0
  56. karotte/mcp_servers/discover_tools.py +125 -0
  57. karotte/mcp_servers/http_mcp_server.py +137 -0
  58. karotte/mcp_servers/mcp_server.py +51 -0
  59. karotte/mcp_servers/resource_profiling_middleware.py +38 -0
  60. karotte/mcp_servers/resource_sampler.py +240 -0
  61. karotte/memory_watch.py +528 -0
  62. karotte/model_catalog.py +92 -0
  63. karotte/model_spec.py +345 -0
  64. karotte/process_utils.py +355 -0
  65. karotte/protected_store.py +196 -0
  66. karotte/providers.py +411 -0
  67. karotte/py.typed +0 -0
  68. karotte/pyproject.toml +124 -0
  69. karotte/reclaim.py +645 -0
  70. karotte/run_config_preprocessors.py +16 -0
  71. karotte/run_helpers.py +829 -0
  72. karotte/runtime.py +15 -0
  73. karotte/save_artifact.py +258 -0
  74. karotte/save_submission.py +250 -0
  75. karotte/schemas/__init__.py +72 -0
  76. karotte/schemas/_import_mcp_types.py +73 -0
  77. karotte/schemas/chat.py +40 -0
  78. karotte/schemas/data_mount.py +37 -0
  79. karotte/schemas/environment_template.py +23 -0
  80. karotte/schemas/evaluation_run_config.py +244 -0
  81. karotte/schemas/http_mcp_server_config.py +18 -0
  82. karotte/schemas/run_state.py +83 -0
  83. karotte/schemas/scoring.py +16 -0
  84. karotte/schemas/transcript.py +194 -0
  85. karotte/schemas/websocket_config.py +6 -0
  86. karotte/step.py +90 -0
  87. karotte/student_misbehavior.py +18 -0
  88. karotte/subprocess.py +582 -0
  89. karotte/task.py +121 -0
  90. karotte/task_factory.py +105 -0
  91. karotte/templates/LICENSE +16 -0
  92. karotte/templates/default/.dockerignore +11 -0
  93. karotte/templates/default/.gitignore +240 -0
  94. karotte/templates/default/.karotte/.gitkeep +0 -0
  95. karotte/templates/default/.python-version +1 -0
  96. karotte/templates/default/CLAUDE.md +3 -0
  97. karotte/templates/default/Containerfile +188 -0
  98. karotte/templates/default/intermediate_data/.gitkeep +0 -0
  99. karotte/templates/default/justfile +86 -0
  100. karotte/templates/default/post_create.py +24 -0
  101. karotte/templates/default/pyproject.toml.jinja +45 -0
  102. karotte/templates/default/root_data/.gitkeep +0 -0
  103. karotte/templates/default/scripts/check_supply_chain_config.py +160 -0
  104. karotte/templates/default/scripts/clear_execstack.py +89 -0
  105. karotte/templates/default/setup_data.py +18 -0
  106. karotte/templates/default/shared_data/.gitkeep +0 -0
  107. karotte/templates/default/src/environment/__init__.py +68 -0
  108. karotte/templates/default/src/environment/check_permissions.py +340 -0
  109. karotte/templates/default/src/environment/fake_model.py +39 -0
  110. karotte/templates/default/src/environment/interpreter_checks.py +55 -0
  111. karotte/templates/default/src/environment/judges/.gitkeep +0 -0
  112. karotte/templates/default/src/environment/paths.py +25 -0
  113. karotte/templates/default/src/environment/submissions.py +34 -0
  114. karotte/templates/default/src/environment/suite.py +125 -0
  115. karotte/templates/default/src/environment/system_prompts.py +23 -0
  116. karotte/templates/default/src/environment/tasks/__init__.py +0 -0
  117. karotte/templates/default/src/environment/tasks/_template/README.md +9 -0
  118. karotte/templates/default/src/environment/tasks/_template/__init__.py +60 -0
  119. karotte/templates/default/src/environment/tasks/_template_suite/README.md +9 -0
  120. karotte/templates/default/src/environment/tasks/_template_suite/__init__.py +101 -0
  121. karotte/templates/default/src/environment/tasks/_template_suite/prompt.py +21 -0
  122. karotte/templates/default/src/environment/tasks/_template_suite/scoring_script.py +44 -0
  123. karotte/templates/default/src/environment/tasks/_template_suite/task_configs/calibration_configs.json +10 -0
  124. karotte/templates/default/src/environment/tasks/_template_suite/task_configs/create_calibration_configs.py +32 -0
  125. karotte/templates/default/src/environment/tasks/_template_suite/task_configs/create_task_configs.py +54 -0
  126. karotte/templates/default/src/environment/tasks/_template_suite/task_configs/task_config.py +30 -0
  127. karotte/templates/default/src/environment/tasks/_template_suite/task_configs/task_configs.json +14 -0
  128. karotte/templates/default/src/environment/tasks/example_task/README.md +24 -0
  129. karotte/templates/default/src/environment/tasks/example_task/__init__.py +96 -0
  130. karotte/templates/default/src/environment/tasks/example_task/scoring_script.py +12 -0
  131. karotte/templates/default/src/environment/tools/.gitkeep +0 -0
  132. karotte/templates/default/student_data/.gitkeep +0 -0
  133. karotte/templates/default/template.toml +3 -0
  134. karotte/templates/default/tests/test_build_venvs.py +287 -0
  135. karotte/templates/default/tests/test_clear_execstack.py +113 -0
  136. karotte/templates/default/tests/test_collect_submission.py +81 -0
  137. karotte/templates/default/tests/test_fake_model.py +29 -0
  138. karotte/templates/default/tests/test_system_prompts.py +84 -0
  139. karotte/templates/default/tests/test_task_discovery.py +527 -0
  140. karotte/templates/default/venvs/README.md +32 -0
  141. karotte/templates/default/venvs/build_venvs.py +182 -0
  142. karotte/templates/default/venvs/student/pyproject.toml.jinja +15 -0
  143. karotte/templates/language-toolchains/Containerfile +135 -0
  144. karotte/templates/language-toolchains/scripts/check_toolchains.py +1661 -0
  145. karotte/templates/language-toolchains/src/environment/elf.py +178 -0
  146. karotte/templates/language-toolchains/src/environment/sandbox.py +499 -0
  147. karotte/templates/language-toolchains/src/environment/sandbox_probe.c +181 -0
  148. karotte/templates/language-toolchains/src/environment/shim.c +146 -0
  149. karotte/templates/language-toolchains/src/environment/toolchain_checks.py +226 -0
  150. karotte/templates/language-toolchains/src/environment/toolchain_config.py +13 -0
  151. karotte/templates/language-toolchains/src/environment/toolchain_grading.py +811 -0
  152. karotte/templates/language-toolchains/src/environment/toolchains.py +2141 -0
  153. karotte/templates/language-toolchains/src/environment/tools/build.py +223 -0
  154. karotte/templates/language-toolchains/template.toml +2 -0
  155. karotte/templates/language-toolchains/tests/test_build_tool.py +256 -0
  156. karotte/templates/language-toolchains/tests/test_sandbox.py +1373 -0
  157. karotte/templates/language-toolchains/tests/test_toolchain_grading.py +110 -0
  158. karotte/templates/language-toolchains/tests/test_toolchains.py +1931 -0
  159. karotte/templates/language-toolchains/toolchains.just +65 -0
  160. karotte/templates/pyproject.base.toml.jinja +19 -0
  161. karotte/templates.py +89 -0
  162. karotte/terminal/__init__.py +0 -0
  163. karotte/terminal/app.py +948 -0
  164. karotte/terminal/console_view.py +65 -0
  165. karotte/terminal/discover_transcripts.py +18 -0
  166. karotte/terminal/run_header.py +90 -0
  167. karotte/terminal/run_view.py +66 -0
  168. karotte/terminal/sidebar.py +206 -0
  169. karotte/terminal/styles.tcss +224 -0
  170. karotte/terminal/transcript_view.py +481 -0
  171. karotte/terminal/websocket_handler.py +48 -0
  172. karotte/text.py +24 -0
  173. karotte/text_files.py +12 -0
  174. karotte/tool_base.py +45 -0
  175. karotte/tools/bash.py +876 -0
  176. karotte/tools/replace_in_file.py +290 -0
  177. karotte/tools/view_image_file.py +139 -0
  178. karotte/tools/view_lines_in_file.py +164 -0
  179. karotte/transcript_markdown.py +261 -0
  180. karotte/transcript_streaming/stream_transcript_to_backend.py +84 -0
  181. karotte/transcript_streaming/stream_transcript_to_stdout.py +182 -0
  182. karotte/transcript_streaming/stream_transcript_to_websocket.py +164 -0
  183. karotte/truncation.py +50 -0
  184. karotte/trusted_bin.py +31 -0
  185. karotte/untrusted_paths.py +64 -0
  186. karotte/update_env.py +676 -0
  187. karotte/uv_supply_chain.py +61 -0
  188. karotte-3.0.4.dist-info/METADATA +310 -0
  189. karotte-3.0.4.dist-info/RECORD +193 -0
  190. karotte-3.0.4.dist-info/WHEEL +4 -0
  191. karotte-3.0.4.dist-info/entry_points.txt +3 -0
  192. karotte-3.0.4.dist-info/licenses/LICENSE +21 -0
  193. karotte-3.0.4.dist-info/licenses/src/karotte/templates/LICENSE +16 -0
karotte/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Preference Model
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
karotte/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # karotte
2
+
3
+ karotte runs LLM agents on tasks and scores the results.
4
+
5
+ You write an *environment*: a Python project with one or more tasks. A task is a
6
+ list of steps. Each step gives the agent instructions and a judge that decides
7
+ whether the agent succeeded. karotte builds the environment into a container
8
+ image, lets the model work inside it through tools like `bash`, and records every
9
+ message, tool call and score in a transcript.
10
+
11
+ The agent runs as an unprivileged user with its own resource limits, a firewall,
12
+ and a disk quota, so a task can hand it a real shell without trusting it.
13
+ The model under test is called the *student*.
14
+
15
+ ## Install
16
+
17
+ karotte needs Python 3.12+, [uv](https://docs.astral.sh/uv/), and docker with
18
+ [buildx](https://github.com/docker/buildx#installing) (or
19
+ podman with `--runtime podman`) for containerized runs. Ubuntu's `docker.io`
20
+ lacks buildx; install `docker-buildx` too. The justfiles need
21
+ [just](https://github.com/casey/just) 1.40 or newer; `uv sync --extra dev`
22
+ installs one into the venv.
23
+
24
+ ```
25
+ uv tool install karotte
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ Create an environment from the `default` template and run its example task:
31
+
32
+ ```
33
+ karotte create-env my_env
34
+ cd my_env
35
+ uv sync --extra dev
36
+ uv run setup_data.py
37
+ uv run karotte create-run-config --model claude-fable-5
38
+ export ANTHROPIC_API_KEY=...
39
+ uv run karotte run --config run_config.json
40
+ ```
41
+
42
+ Commands that load your tasks (`create-run-config`, `run`, `tasks list`,
43
+ `check`) have to run in the environment's venv, hence `uv run`.
44
+ `setup_data.py` prepares data the image needs, such as model weights; the
45
+ `default` template's version does nothing. `karotte check` loads every task and
46
+ tool and fails if one doesn't load; the image build runs it too.
47
+
48
+ `karotte run` builds the image, runs the task, and writes the transcript to
49
+ `out/transcript.json`. Files the task saves as artifacts go to
50
+ `out/<run_id>_artifacts/`. `karotte dashboard out/` shows the transcripts in `out/`.
51
+ `karotte models list` shows which models karotte knows; model ids are passed to
52
+ [litellm](https://docs.litellm.ai/). The API key is `model_api_key` in the run
53
+ config. `create-run-config` sets it to the key variable of the model's
54
+ provider, such as `$ANTHROPIC_API_KEY` or `$OPENAI_API_KEY`, which karotte reads
55
+ from your environment when the run starts. `--model-api-key` sets another.
56
+
57
+ Useful while you work on a task:
58
+
59
+ - `uv run karotte run --config run_config.json --dev` skips the image build and
60
+ mounts your `src/environment/` into the container.
61
+ - `uv run karotte tasks list` shows the tasks the environment defines;
62
+ `create-run-config --task <task-id>` picks one.
63
+
64
+ ### The container image
65
+
66
+ Every `karotte run` without `--dev` rebuilds the image first. podman and docker
67
+ reuse cached layers, so an unchanged environment builds quickly.
68
+
69
+ `--dev` skips the build and runs the existing image, with your
70
+ `src/environment/` mounted over the installed copy. Rebuild after changing
71
+ dependencies, the `Containerfile` or data.
72
+
73
+ `karotte build` builds the same image without running a task. `run` always uses
74
+ the image tagged `karotte`, which is `build`'s default tag, and has no option to
75
+ pick another. `build --tag` is for images you use elsewhere.
76
+
77
+ ### Model endpoints and proxies
78
+
79
+ `karotte run --proxy <url>` sends the model calls to `<url>` instead of the
80
+ provider. Pass the root URL, without `/v1`:
81
+
82
+ - Claude models go to `<url>/v1/messages`. karotte sets `ANTHROPIC_BASE_URL` to
83
+ `<url>` in the container, and litellm adds `/v1/messages`.
84
+ - Other models go to `<url>/v1`, so the endpoint needs an OpenAI-compatible API
85
+ there. `vertex_ai/` models ignore the proxy.
86
+
87
+ The same flag points karotte at any endpoint that serves these paths. The URL is
88
+ used inside the container, where `localhost` is the container itself. Variables
89
+ like `ANTHROPIC_BASE_URL` in your shell don't reach the container. With
90
+ `--no-containerized`, karotte sets `ANTHROPIC_BASE_URL` and `KAROTTE_PROXY_URL`
91
+ in its own process instead, unless one of them is already set.
92
+ `--no-containerized` only runs inside a karotte container image, where
93
+ `KAROTTE_CONTAINERIZED` is set.
94
+
95
+ A key is needed only if the endpoint asks for one. karotte still sends
96
+ `model_api_key`; if that names a variable such as `$OPENAI_API_KEY` that is
97
+ unset, `--proxy` fills in a placeholder.
98
+
99
+ Without `--proxy`, karotte uses the URL a plugin registers under
100
+ `karotte.default_proxy_url` (see [Plugins](#plugins)). Without such a plugin,
101
+ calls go straight to the provider. `--no-proxy` ignores the plugin's URL.
102
+
103
+ `KAROTTE_INFERENCE_SERVICE_TIER=priority` asks Fireworks and Vertex AI Gemini
104
+ for their priority tier, `auto` only once the provider runs out of capacity.
105
+ Unset, the default, uses the provider's default tier.
106
+
107
+ ## Writing tasks
108
+
109
+ Each task is a package in `src/environment/tasks/`: a directory whose
110
+ `__init__.py` defines the `Task` class. Plain files and directories starting with
111
+ `_` are skipped. `uv run just create-task <task-id>` copies the task template
112
+ (`just` comes with `uv sync --extra dev` and lives in the venv). A minimal task:
113
+
114
+ ```python
115
+ import re
116
+
117
+ from karotte import Step, Task
118
+ from karotte.judges import RegexJudge
119
+
120
+
121
+ class FindPython(Task):
122
+ id = "find-python"
123
+
124
+ @property
125
+ def system_prompt(self) -> str:
126
+ return "You are working in a Linux shell."
127
+
128
+ @property
129
+ def steps(self):
130
+ return [FindPythonStep(config=self.config)]
131
+
132
+ @property
133
+ def tools(self):
134
+ return ["bash"]
135
+
136
+
137
+ class FindPythonStep(Step):
138
+ @property
139
+ def instructions(self) -> str:
140
+ return "Find the path to your Python executable. Answer with `path: <path>`."
141
+
142
+ @property
143
+ def judge(self):
144
+ return RegexJudge([re.compile(r"path: .*/python3?")])
145
+ ```
146
+
147
+ Judges in `karotte.judges`: `RegexJudge` matches the final message,
148
+ `ExecutableJudge` runs a scoring script, `RubricJudge` asks an LLM to grade
149
+ against a rubric, and `Judge` is the base class for your own. The example task
150
+ in the `default` template shows submissions, hints, and hooks that run before
151
+ scoring.
152
+
153
+ ## Templates
154
+
155
+ A template is a directory of files, rendered with
156
+ [Jinja](https://jinja.palletsprojects.com/) into a new environment. karotte ships
157
+ two:
158
+
159
+ - `default`: a CPU environment with an example task. Every other template builds
160
+ on it.
161
+ - `language-toolchains`: gives the agent exactly one language toolchain per task.
162
+
163
+ `karotte templates list` shows every installed template. Templates stack:
164
+ `karotte create-env my_env --template default --template language-toolchains`
165
+ renders `default` and then `language-toolchains` on top. A later template can
166
+ replace a file or override a Jinja block in it:
167
+
168
+ ```
169
+ {% extends "default/CLAUDE.md" %}
170
+ {% block claude_md -%}
171
+ New content
172
+ {% endblock %}
173
+ ```
174
+
175
+ `karotte update` brings an existing environment up to the latest templates with
176
+ a 3-way merge, so your own edits survive.
177
+
178
+ ### Writing a template package
179
+
180
+ Templates can live in their own Python package. Put the template directories
181
+ (each with a `template.toml`) under one directory and register it under the
182
+ `karotte.templates` entry point:
183
+
184
+ ```toml
185
+ [project.entry-points."karotte.templates"]
186
+ my_templates = "my_package:TEMPLATES_DIR"
187
+ ```
188
+
189
+ `TEMPLATES_DIR` is a path to that directory. A `template.toml` holds the fields of
190
+ [`EnvironmentTemplate`](https://github.com/preferencemodel/karotte/blob/main/src/karotte/schemas/environment_template.py), for
191
+ example:
192
+
193
+ ```toml
194
+ description = "Adds a Rust toolchain."
195
+ requires = ["default"]
196
+ ```
197
+
198
+ Install the package next to karotte (`uvx --with my-package karotte create-env
199
+ ...`). karotte records it in the environment's manifest, so `karotte update`
200
+ pulls it in again.
201
+
202
+ ## Plugins
203
+
204
+ Besides templates, a package can extend karotte through these entry points:
205
+
206
+ | Entry point | Points at | Effect |
207
+ | --- | --- | --- |
208
+ | `karotte.cli` | a Typer app | Adds a subcommand named after the entry point. |
209
+ | `karotte.run_config_preprocessors` | `f(config) -> config` | Rewrites the run config before a run, in entry point name order. |
210
+ | `karotte.default_proxy_url` | a string | Default for `karotte run --proxy`. |
211
+ | `karotte.harness_secret_env` | a list of names | Environment variables hidden from the agent. |
212
+ | `karotte.platform_tooling_dirs` | a list of paths | Directories where a platform mounts its own tooling into every container; hidden from graded toolchain runs. |
213
+ | `karotte.age_delay_exemptions` | a list of package names | More packages exempt from uv's `exclude-newer` delay (karotte always is). |
214
+ | `karotte.update_migrations` | an object with `prepare`, `tool` and `migrate` | Moves envs made by an older release to the current names during `karotte update`. |
215
+ | `karotte.default_hardware` | a string | `required_hardware` of tasks that set none. karotte itself knows no hardware names. |
216
+ | `karotte.hardware_limits` | `f(hardware) -> HardwareLimits \| None` | Memory and disk a sandbox on that hardware holds. Without it, the agent's memory limit is the sandbox's cgroup limit or RAM, less 1 GiB for the harness, and none where cgroups don't work. |
217
+ | `karotte.container_run_args` | `f(task, runtime) -> list[str]` | Extra arguments for the container engine's `run` (docker, podman or nerdctl), e.g. to pass devices through, in entry point name order. Raising refuses the launch with the exception's message. |
218
+
219
+ A plugin that fails to load is skipped with a warning, except a run config
220
+ preprocessor: that one fails the run.
221
+
222
+ ## Connecting a backend
223
+
224
+ Without a backend, karotte writes the transcript to a file. To collect runs
225
+ centrally, set `backend_uri` in the run config. karotte then calls these HTTP
226
+ endpoints on it, with `run_id` as a query parameter (presign has it in the body):
227
+
228
+ | Request | Purpose |
229
+ | --- | --- |
230
+ | `POST /api/internal/create_transcript` | Start a transcript. |
231
+ | `GET /api/internal/transcript_length` | Number of stored events, 404 if none. |
232
+ | `POST /api/internal/append_transcript` | Body `{"event": ..., "seq": n}`. Writing the same `seq` again must overwrite, not append. |
233
+ | `POST /api/internal/update_run_state` | Body with `status` (`running`, `passed`, `failed`, `error`), `score`, and token counts. |
234
+ | `POST /api/artifacts/presign` | Body `{"run_id", "artifact_paths"}`, returns `{"presigned_urls": {path: url}}`. karotte PUTs each file gzipped. |
235
+
236
+ With the `external` agent the backend also supplies the model's messages:
237
+ `GET /api/internal/get_message` returns the next message (404 while there is
238
+ none) and `POST /api/internal/delete_message` consumes it.
239
+
240
+ Requests carry `Authorization: Bearer <token>` if the file at
241
+ `KAROTTE_BACKEND_TOKEN_PATH` (default `/var/run/secrets/service-account-token`)
242
+ exists. The events are the models in
243
+ [`karotte.schemas.transcript`](https://github.com/preferencemodel/karotte/blob/main/src/karotte/schemas/transcript.py).
244
+
245
+ ## Development
246
+
247
+ ```
248
+ just lint
249
+ just test
250
+ just test-template default
251
+ ```
252
+
253
+ Every merge to `main` is released. The patch version is the commit count.
254
+
255
+ ## License
256
+
257
+ karotte is under the [MIT license](https://github.com/preferencemodel/karotte/blob/main/LICENSE). The templates in
258
+ `src/karotte/templates/` are under [MIT No Attribution](https://github.com/preferencemodel/karotte/blob/main/src/karotte/templates/LICENSE)
259
+ (`MIT-0`), so environments created from them need no license notice.
260
+
261
+ ### Third-party software in built images
262
+
263
+ Images built from the templates contain third-party software under its own
264
+ licenses. They are based on Amazon Linux 2023. The `language-toolchains`
265
+ template installs GPL and LGPL software (gcc, GnuCOBOL, GNU Prolog, Free Pascal,
266
+ the libraries bundled with Julia, and others), Amazon Corretto (GPL-2.0 with the
267
+ Classpath Exception) and Clojure (EPL-1.0) for the languages you enable. Mojo,
268
+ off by default, is under Modular's proprietary license and has its own terms even
269
+ if you never share the image. The rest only matters if you redistribute a built
270
+ image, for example by pushing it to a public registry.
karotte/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """karotte: A library for creating and running AI agent evaluation environments."""
2
+
3
+ from karotte.confinement import Contract as Contract
4
+ from karotte.confinement import FileLimit as FileLimit
5
+ from karotte.confinement import ResourceLimits as ResourceLimits
6
+ from karotte.confinement import get_resource_limits as get_resource_limits
7
+ from karotte.confinement import limit_resources as limit_resources
8
+ from karotte.judges.judge import Judge as Judge
9
+ from karotte.process_utils import UnreapableCohortError as UnreapableCohortError
10
+ from karotte.process_utils import kill_processes as kill_processes
11
+ from karotte.protected_store import ProtectedStore as ProtectedStore
12
+ from karotte.reclaim import ReclaimError as ReclaimError
13
+ from karotte.reclaim import delete_files as delete_files
14
+ from karotte.runtime import Runtime as Runtime
15
+ from karotte.save_artifact import save_artifact as save_artifact
16
+ from karotte.save_submission import save_submission as save_submission
17
+ from karotte.schemas.evaluation_run_config import (
18
+ EvaluationRunConfig as EvaluationRunConfig,
19
+ )
20
+ from karotte.step import Step as Step
21
+ from karotte.student_misbehavior import (
22
+ StudentMisbehaviorError as StudentMisbehaviorError,
23
+ )
24
+ from karotte.task import Task as Task
25
+ from karotte.task_factory import StepConfig as StepConfig
26
+ from karotte.task_factory import create_task as create_task
27
+ from karotte.tool_base import ToolBase as ToolBase
28
+ from karotte.tool_base import ToolConfigWriter as ToolConfigWriter
29
+
30
+ __all__ = [
31
+ "Task",
32
+ "Step",
33
+ "Judge",
34
+ "EvaluationRunConfig",
35
+ "ToolBase",
36
+ "ToolConfigWriter",
37
+ "ProtectedStore",
38
+ "save_artifact",
39
+ "save_submission",
40
+ "create_task",
41
+ "StepConfig",
42
+ "Runtime",
43
+ "kill_processes",
44
+ "UnreapableCohortError",
45
+ "delete_files",
46
+ "ReclaimError",
47
+ "StudentMisbehaviorError",
48
+ "limit_resources",
49
+ "get_resource_limits",
50
+ "ResourceLimits",
51
+ "Contract",
52
+ "FileLimit",
53
+ ]
@@ -0,0 +1,52 @@
1
+ """Pluggable agents for the evaluation runner.
2
+
3
+ An `Agent` produces a run's transcript one step at a time; the runner keeps the
4
+ task lifecycle (hooks, judging, artifacts, event bookkeeping). Builtin and
5
+ external agents share `MessageLoopAgent`, differing only in their
6
+ `MessageSource` -- builtin drives the litellm API loop itself, external fetches
7
+ messages from the backend (training rollouts). Fake replays env-authored
8
+ messages.
9
+ """
10
+
11
+ from karotte.agents.agent import (
12
+ Agent,
13
+ EmptyTurnLimitReachedError,
14
+ RunContext,
15
+ TurnLimitReachedError,
16
+ )
17
+ from karotte.agents.backend_source import EXTERNAL_MESSAGE_TIMEOUT_S, BackendSource
18
+ from karotte.agents.builtin import BuiltinAgent
19
+ from karotte.agents.builtin_source import BuiltinSource
20
+ from karotte.agents.cli_agent import (
21
+ CliAgent,
22
+ cli_agent_types,
23
+ get_cli_agent_type,
24
+ register_cli_agent,
25
+ )
26
+ from karotte.agents.external import ExternalAgent
27
+ from karotte.agents.fake_source import FakeSource
28
+ from karotte.agents.message_loop import MessageLoopAgent
29
+ from karotte.agents.message_source import MessageSource
30
+
31
+ # Import agent modules for their `register_cli_agent` side effects.
32
+ from karotte.agents.mistral_vibe import MistralVibeAgent
33
+
34
+ __all__ = [
35
+ "EXTERNAL_MESSAGE_TIMEOUT_S",
36
+ "Agent",
37
+ "BackendSource",
38
+ "BuiltinAgent",
39
+ "BuiltinSource",
40
+ "CliAgent",
41
+ "EmptyTurnLimitReachedError",
42
+ "ExternalAgent",
43
+ "FakeSource",
44
+ "MessageLoopAgent",
45
+ "MessageSource",
46
+ "MistralVibeAgent",
47
+ "RunContext",
48
+ "TurnLimitReachedError",
49
+ "cli_agent_types",
50
+ "get_cli_agent_type",
51
+ "register_cli_agent",
52
+ ]
@@ -0,0 +1,97 @@
1
+ from collections.abc import AsyncGenerator
2
+ from dataclasses import dataclass
3
+ from typing import ClassVar, Literal, Protocol, final
4
+
5
+ from fastmcp import Client
6
+ from fastmcp.client import StreamableHttpTransport
7
+ from litellm import ChatCompletionToolParam
8
+
9
+ from karotte.schemas.evaluation_run_config import EvaluationRunConfig
10
+ from karotte.schemas.transcript import Event, Transcript
11
+
12
+
13
+ class TurnLimitReachedError(Exception):
14
+ """Raised when the evaluation run exceeds the configured turn limit."""
15
+
16
+
17
+ class StepTimeLimitReachedError(Exception):
18
+ """Raised when a step exceeds its configured wall-clock time limit."""
19
+
20
+
21
+ class StepContextWindowLimitReachedError(Exception):
22
+ """Raised when a step exceeds its configured context-window token limit."""
23
+
24
+
25
+ class EmptyTurnLimitReachedError(Exception):
26
+ """Raised when the model produced neither text nor tool calls too many turns
27
+ in a row, so nudging it to continue has stopped working."""
28
+
29
+
30
+ @final
31
+ @dataclass
32
+ class RunContext:
33
+ """What an agent needs to solve steps within a run.
34
+
35
+ The runner owns the transcript and applies event bookkeeping; the agent
36
+ reads `transcript.messages` for conversation history and executes tool calls
37
+ against `mcp_client`. One `RunContext` per run, passed to `Agent.start`.
38
+ """
39
+
40
+ config: EvaluationRunConfig
41
+ mcp_client: Client[StreamableHttpTransport]
42
+ tools: list[ChatCompletionToolParam]
43
+ transcript: Transcript
44
+
45
+
46
+ class Agent(Protocol):
47
+ """Produces a run's transcript, one step at a time.
48
+
49
+ The runner keeps the task lifecycle (hooks, judging, artifacts, event
50
+ bookkeeping); the agent decides how messages and tool calls get produced.
51
+ An agent instance is stateful across steps within a run.
52
+ """
53
+
54
+ allows_student_mcp_access: ClassVar[bool]
55
+ """Whether the student user may reach the MCP port.
56
+
57
+ True only for agents that run in-container as the student and make native
58
+ MCP tool calls (CLI agents). Builtin/external agents keep the port blocked:
59
+ karotte executes their tool calls itself, so student access would only let
60
+ task code bypass transcript recording.
61
+ """
62
+
63
+ native_tool_names: ClassVar[frozenset[str]]
64
+ """Tools this agent supplies itself, and which the runner therefore
65
+ omits when registering the task's tools. A CLI agent with its own bash/file
66
+ tools lists them here so it uses its native ones instead of duplicates over
67
+ MCP. Empty for agents that rely entirely on MCP tools.
68
+ """
69
+
70
+ async def start(self, ctx: RunContext) -> None:
71
+ """Verify/configure the agent for the run."""
72
+ ...
73
+
74
+ def run_step(
75
+ self,
76
+ instructions: str,
77
+ time_limit_seconds: float | None = None,
78
+ on_time_limit: Literal["error", "score"] = "error",
79
+ context_window_limit: int | None = None,
80
+ on_context_window_limit: Literal["error", "score"] = "error",
81
+ ) -> AsyncGenerator[Event]:
82
+ """Solve one step, yielding raw transcript events.
83
+
84
+ `time_limit_seconds` bounds the step's wall-clock duration; when it
85
+ elapses the step either aborts (`on_time_limit="error"`) or ends so it
86
+ can be scored (`on_time_limit="score"`). `None` disables the limit.
87
+
88
+ `context_window_limit` bounds the step's context-window length (the input
89
+ tokens of the most recent turn); when it is exceeded the step either
90
+ aborts (`on_context_window_limit="error"`) or ends so it can be scored
91
+ (`on_context_window_limit="score"`). `None` disables the limit.
92
+ """
93
+ ...
94
+
95
+ async def stop(self) -> None:
96
+ """Release any resources held for the run."""
97
+ ...
@@ -0,0 +1,47 @@
1
+ import time
2
+ from collections.abc import AsyncGenerator
3
+ from typing import final
4
+
5
+ from litellm import ChatCompletionToolParam
6
+ from loguru import logger
7
+
8
+ from karotte.backend_client import BackendClient
9
+ from karotte.schemas.chat import Message
10
+ from karotte.schemas.transcript import Event
11
+
12
+ # Maximum time to wait for a message from the model before giving up.
13
+ EXTERNAL_MESSAGE_TIMEOUT_S = 120 * 60
14
+
15
+
16
+ @final
17
+ class BackendSource:
18
+ """Long-polls the backend for the next message produced by an external model
19
+ (training rollouts). karotte still drives the step loop and executes the tool
20
+ calls the message contains."""
21
+
22
+ def __init__(self, backend_client: BackendClient, run_id: str) -> None:
23
+ self.backend_client = backend_client
24
+ self.run_id = run_id
25
+
26
+ async def collect(
27
+ self,
28
+ messages: list[Message], # pyright: ignore[reportUnusedParameter]
29
+ tools: list[ChatCompletionToolParam], # pyright: ignore[reportUnusedParameter]
30
+ ) -> AsyncGenerator[Event]:
31
+ start = time.time()
32
+ last_log = time.time()
33
+ while True:
34
+ elapsed = time.time() - start
35
+ if elapsed > EXTERNAL_MESSAGE_TIMEOUT_S:
36
+ raise TimeoutError(
37
+ f"Timed out after {elapsed:.0f}s waiting for a response from the model"
38
+ )
39
+ if time.time() - last_log > 30:
40
+ logger.warning(f"Still waiting for new message, {elapsed:.0f}s elapsed")
41
+ last_log = time.time()
42
+ # get_message is a long-poll operation so we don't need extra sleep
43
+ msg_added_evt = await self.backend_client.get_message(self.run_id)
44
+ if msg_added_evt is not None:
45
+ yield msg_added_evt
46
+ await self.backend_client.delete_message(self.run_id)
47
+ return
@@ -0,0 +1,13 @@
1
+ from typing import final
2
+
3
+ from karotte.agents.builtin_source import BuiltinSource
4
+ from karotte.agents.message_loop import MessageLoopAgent
5
+ from karotte.schemas.evaluation_run_config import EvaluationRunConfig
6
+
7
+
8
+ @final
9
+ class BuiltinAgent(MessageLoopAgent):
10
+ """karotte drives the model itself via the litellm API loop."""
11
+
12
+ def __init__(self, config: EvaluationRunConfig) -> None:
13
+ super().__init__(BuiltinSource(config))