feat(agent): agent-selectable GPU/CPU per execute-code call with project allowance - #160
Closed
lucastononro wants to merge 4 commits into
Closed
feat(agent): agent-selectable GPU/CPU per execute-code call with project allowance#160lucastononro wants to merge 4 commits into
lucastononro wants to merge 4 commits into
Conversation
…ect allowance
- SandboxConfig gains allowed_gpus (canonical labels, validated) and
max_timeout; shared resolver in services/compute_allowance.py used by
both the handler and the prompt renderer so they never disagree.
Effective allowance = {cpu} ∪ profile GPUs ∪ allowed_gpus; absent
config keeps existing projects unchanged.
- execute-code gains optional gpu/timeout args. Precedence: explicit gpu
> heavy profile > default profile. Denied GPUs return a structured
tool error naming the allowed set; timeouts clamp to the project cap.
tool_start events now carry the gpu/timeout actually used.
- _format_compute_env rewritten: renders the allowance with per-option
guidance and ~$/hr from the active provider's rates, the timeout cap,
and choice guidance — injected into the system prompt at session
start for every execute-code agent.
- Project Settings UI: allowed-GPU checkbox grid + max-timeout field;
GPU_OPTIONS fixed to canonical labels (stale 'A100' broke billing —
legacy labels are normalized leniently on read).
- trainer.yaml / SKILL.md guidance updated to prefer explicit gpu=.
Closes #146
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GrGFBtdZv7qtgn8KQEQEB
lucastononro
force-pushed
the
feat/agent-gpu-choice
branch
from
July 19, 2026 20:16
e83a9b3 to
39d82b0
Compare
…meout cap - GPU denial now emits tool_start before tool_end (every other handler path does; the UI's active-tool state machine depends on the pair) - an owner-set max_timeout now caps profile-fallback timeouts too, not just the explicit timeout= arg (heavy=True without timeout= used to bypass it) + regression tests - _gpu_hourly_usd logs when pricing resolution fails instead of silently dropping prices from the prompt
# Conflicts: # frontend/src/components/ProjectSettingsModal.tsx
lucastononro
added a commit
that referenced
this pull request
Jul 29, 2026
…project allowance (closes #146)
Owner
Author
|
Merged into integration branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #146
What
Lets the LLM agent choose which GPU/CPU to spin up per
execute-codecall, constrained by a project-level allowance, with the allowance presented in the system prompt at session start so the model plans with it.Allowance (project level, JSON — no DDL migration)
SandboxConfiggainsallowed_gpus(canonical labels, validated + deduped,[]→None) andmax_timeout(10–7200 s). One shared resolver (backend/services/compute_allowance.py) feeds both the handler enforcement and the prompt renderer, so they can never disagree:{cpu} ∪ {profile GPUs} ∪ allowed_gpus— profile GPUs are implicitly allowed (the agent can already reach them viaheavy=True)max_timeoutabsent →max(profile timeouts, SANDBOX_TIMEOUT)so the agent can't self-grant longer runs than the owner ever configuredA100→A100-40GB, case-insensitive) — old project configs keep workingAgent surface
execute-codegains optionalgpuandtimeoutargs. Precedence: explicitgpu>heavyprofile > default profile.is_error: true) naming the allowed set,run_codenever called — the agent self-correctsgpu="cpu"forces a CPU sandbox (overrides evenheavy=True)timeoutclamps to[10, max_timeout]; garbage falls back to the profiletool_startevents now carry thegpu/timeoutactually used so the UI can show what ranrun_codealready records the GPU actually passed, per (provider, gpu) ratesPrompt presentation
_format_compute_env()(injected at session start for every agent withexecute-code) now renders the allowance. Sample:Prices come from the active provider's
sandbox.ymlrates and are omitted gracefully if pricing fails.Config surface
ProjectCreate/ProjectUpdatealready typedSandboxConfig— pass-through is free.GPU_OPTIONSfixed to canonical labels (the stale'A100'value silently broke billing; addedA100-80GB,H100).trainer.yaml,execute-code/train-tabularSKILL.md): prefer explicitgpu=sized to the job,heavy=truestays the fallback.Out of scope (noted follow-ups)
✅ Verified automatically
test_compute_allowance.py(resolver semantics — implicit/explicit allowance, timeout fallbacks, legacyA100normalization, cost-ascending ordering; schema validation — unknown labels rejected, dedupe,[]→None, bounds; prompt rendering — allowed labels listed, disallowed absent, cap shown, prices degrade gracefully) andtest_execute_code_handler.py(allowed GPU passes torun_code, denied GPU → structured error naming the allowed set without callingrun_code,gpu="cpu"overridesheavy=True, case-insensitivity, full precedence matrix, timeout clamp both directions, garbage timeout falls back to profile,tool_startcarries gpu/timeout, no-config = cpu-only).gpu/timeoutargs survivediscover_skills → get_normalized_specs(the exact schema Claude/OpenAI/Gemini receive, enum + bounds intact); the compute block is gated to agents that actually haveexecute-code(trainer/eda/chat verified); a legacy project config withgpu: "A100"renders normalized without crashing.ruff check+formatclean; frontendtsc --noEmitandnext buildclean.🧑💻 Requires a human (running app; Modal is fine — no RunPod needed)
T4, max timeout 900), start a session, and confirm the "Compute environment" block in the agent's behavior — easiest probe: ask "what hardware are you allowed to use?" and check the answer matches the settings.gpu: cpu/gpu: T4and (for a real GPU run, costs cents)torch.cuda.is_available()is True.null(profiles-only behavior).allowed_gpusin its config) — sessions behave exactly as before, and the prompt block shows only cpu + the profile GPUs.timeout=3600— execution is killed at ~60 s and the timeout tool error mentions the cap.🤖 Generated with Claude Code
https://claude.ai/code/session_012GrGFBtdZv7qtgn8KQEQEB
Greptile Summary
This PR adds agent-selectable GPU/CPU per
execute-codecall, constrained by a project-level allowance stored inSandboxConfig. A shared resolver (compute_allowance.py) feeds both the handler enforcement and the system-prompt renderer so they can never disagree on what hardware is permitted.compute_allowance.py:resolve_compute_allowancebuilds the effective GPU set ({"cpu"} ∪ profile GPUs ∪ allowed_gpus), computesmax_timeout, and providesnormalize_gpu(with legacy-alias support for old"A100"values) andclamp_timeout. Well-covered by 36 new tests.execute-codehandler: adds optionalgpuandtimeoutargs with allowance enforcement; denied GPUs return a structuredis_errorresponse without callingrun_code; ownermax_timeoutnow caps the profile fallback path too (addressing previous gap).GPU_OPTIONScorrected to canonical labels (A100-40GB,A100-80GB,H100added, stale'A100'removed).Confidence Score: 5/5
Safe to merge; the only finding is a minor payload inconsistency in a UI-facing event field that has no impact on execution correctness.
The core allowance enforcement is well-designed — a shared resolver guarantees the prompt and the handler agree, the denial path now correctly emits tool_start before tool_end (previous gap closed), and the owner max_timeout now caps the profile fallback path. The 36 new tests cover precedence, clamping, legacy normalization, and event ordering. The only issue is that the denial-path tool_start emits timeout: None rather than a resolved integer, which is a cosmetic inconsistency with no effect on correctness or security.
Files Needing Attention: handler.py denial-path tool_start payload (timeout field)
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Agent calls execute-code\ngpu=X, timeout=T, heavy=H] --> B[resolve_compute_allowance\nsandbox_config] B --> C{requested_gpu\nprovided?} C -- yes --> D[normalize_gpu] D --> E{permits label?} E -- no --> F[emit tool_start + tool_end\nreturn structured error\nis_error=true] E -- yes --> G[gpu = label\ncpu → None] C -- no --> H[gpu = profile.get gpu\nheavy→training else default] G --> I[clamp timeout\nif requested_timeout] H --> I I --> J[timeout = min\ntimeout or sandbox_timeout\nallowance.max_timeout] J --> K[emit tool_start\ngpu/timeout] K --> L[run_code] L --> M{error?} M -- SandboxTimeoutError --> N[emit tool_end\ntimed_out=true\nis_error=true] M -- other error --> O[emit tool_end\nis_error=true] M -- success --> P[detect_new_files\nemit tool_end\nreturn output]Reviews (3): Last reviewed commit: "Merge branch 'staging-v0.0.5' into feat/..." | Re-trigger Greptile