Skip to content

feat(agent): agent-selectable GPU/CPU per execute-code call with project allowance - #160

Closed
lucastononro wants to merge 4 commits into
feat/runpod-providerfrom
feat/agent-gpu-choice
Closed

feat(agent): agent-selectable GPU/CPU per execute-code call with project allowance#160
lucastononro wants to merge 4 commits into
feat/runpod-providerfrom
feat/agent-gpu-choice

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes #146

Depends on #145 (feat/runpod-provider) — this branch is based on it and builds on the compute-provider abstraction (canonical GPU labels, provider-aware billing, SandboxTimeoutError). Merge #145 first; this PR will then show only its own commit.

What

Lets the LLM agent choose which GPU/CPU to spin up per execute-code call, 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)

SandboxConfig gains allowed_gpus (canonical labels, validated + deduped, []None) and max_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:

  • effective allowance = {cpu} ∪ {profile GPUs} ∪ allowed_gpus — profile GPUs are implicitly allowed (the agent can already reach them via heavy=True)
  • absent config → implicit allowance only; existing projects behave exactly as before
  • max_timeout absent → max(profile timeouts, SANDBOX_TIMEOUT) so the agent can't self-grant longer runs than the owner ever configured
  • legacy labels normalized leniently (A100A100-40GB, case-insensitive) — old project configs keep working

Agent surface

execute-code gains optional gpu and timeout args. Precedence: explicit gpu > heavy profile > default profile.

  • denied GPU → structured tool error (is_error: true) naming the allowed set, run_code never called — the agent self-corrects
  • gpu="cpu" forces a CPU sandbox (overrides even heavy=True)
  • timeout clamps to [10, max_timeout]; garbage falls back to the profile
  • tool_start events now carry the gpu/timeout actually used so the UI can show what ran
  • billing needed no change — run_code already records the GPU actually passed, per (provider, gpu) rates

Prompt presentation

_format_compute_env() (injected at session start for every agent with execute-code) now renders the allowance. Sample:

## Compute environment for `execute-code`

Each call provisions a fresh sandbox. You choose the compute per
call with the optional `gpu` argument:

**Allowed hardware** (values accepted for `gpu`):
  - `cpu` — CPU only — EDA, plotting, sklearn/xgboost/lightgbm (~$0.05/hr)
  - `T4` — 16GB entry GPU — small fine-tunes, light inference (~$0.59/hr)
  - `A10G` — 24GB mid-tier — solid training workhorse (~$1.10/hr)
  - `A100-80GB` — 80GB — very large models / long contexts (~$2.50/hr)

**Timeout**: pass `timeout` (seconds, per call; max 3600s — higher values are clamped). Defaults: 600s (default profile) / 1800s (`heavy=True`).

**How to choose**:
- Omit `gpu` → profile fallback: `heavy=False` = default profile (cpu), `heavy=True` = training profile (A10G).
- Prefer the cheapest hardware that fits. ...

Prices come from the active provider's sandbox.yml rates and are omitted gracefully if pricing fails.

Config surface

  • Backend: ProjectCreate/ProjectUpdate already typed SandboxConfig — pass-through is free.
  • Frontend: Project Settings gains an Agent GPU allowance checkbox grid (CPU pinned always-on) + max-timeout field; GPU_OPTIONS fixed to canonical labels (the stale 'A100' value silently broke billing; added A100-80GB, H100).
  • Agent docs updated (trainer.yaml, execute-code/train-tabular SKILL.md): prefer explicit gpu= sized to the job, heavy=true stays the fallback.

Out of scope (noted follow-ups)

  • Notebook kernels stay CPU-only (agent-chosen GPU on a long-lived kernel is an idle-billing hazard).
  • Per-session allowance overrides (project-level only).

✅ Verified automatically

  • 36 new tests: test_compute_allowance.py (resolver semantics — implicit/explicit allowance, timeout fallbacks, legacy A100 normalization, cost-ascending ordering; schema validation — unknown labels rejected, dedupe, []None, bounds; prompt rendering — allowed labels listed, disallowed absent, cap shown, prices degrade gracefully) and test_execute_code_handler.py (allowed GPU passes to run_code, denied GPU → structured error naming the allowed set without calling run_code, gpu="cpu" overrides heavy=True, case-insensitivity, full precedence matrix, timeout clamp both directions, garbage timeout falls back to profile, tool_start carries gpu/timeout, no-config = cpu-only).
  • Integration checks through the real plumbing (run locally): the new gpu/timeout args survive discover_skills → get_normalized_specs (the exact schema Claude/OpenAI/Gemini receive, enum + bounds intact); the compute block is gated to agents that actually have execute-code (trainer/eda/chat verified); a legacy project config with gpu: "A100" renders normalized without crashing.
  • Full backend suite: 379 passed, 8 skipped; ruff check + format clean; frontend tsc --noEmit and next build clean.

🧑‍💻 Requires a human (running app; Modal is fine — no RunPod needed)

  1. Prompt visibility: set an allowance in Project Settings (e.g. allow 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.
  2. Agent picks a GPU end-to-end: ask the agent to "run a quick check on cpu" and then something like "train on the T4" — verify the tool events show gpu: cpu / gpu: T4 and (for a real GPU run, costs cents) torch.cuda.is_available() is True.
  3. Denial round-trip: ask for a disallowed GPU ("use an H100") — the agent should get the error naming the allowed set and recover by choosing an allowed option, not crash the session.
  4. Settings UI: toggle allowance checkboxes + max timeout, Save, reopen the modal — values persist; clearing all checkboxes saves null (profiles-only behavior).
  5. Legacy project: open a project created before this PR (no allowed_gpus in its config) — sessions behave exactly as before, and the prompt block shows only cpu + the profile GPUs.
  6. Timeout clamp in anger: set max timeout low (e.g. 60), ask the agent to run with 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-code call, constrained by a project-level allowance stored in SandboxConfig. 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.

  • New compute_allowance.py: resolve_compute_allowance builds the effective GPU set ({"cpu"} ∪ profile GPUs ∪ allowed_gpus), computes max_timeout, and provides normalize_gpu (with legacy-alias support for old "A100" values) and clamp_timeout. Well-covered by 36 new tests.
  • execute-code handler: adds optional gpu and timeout args with allowance enforcement; denied GPUs return a structured is_error response without calling run_code; owner max_timeout now caps the profile fallback path too (addressing previous gap).
  • Frontend: Project Settings gains an agent GPU allowance checkbox grid and max-timeout field; GPU_OPTIONS corrected to canonical labels (A100-40GB, A100-80GB, H100 added, 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

Filename Overview
backend/services/compute_allowance.py New shared resolver: resolve_compute_allowance, normalize_gpu (with legacy-alias support), and clamp_timeout — clean, well-tested, and used by both enforcement and prompt rendering.
backend/skills/execute-code/handler.py Adds GPU/timeout selection with allowance enforcement. The denial-path tool_start now correctly precedes tool_end (previous issue fixed), but its timeout field can be None when the profile has no explicit timeout, inconsistent with the normal path.
backend/schemas.py Adds CANONICAL_GPUS tuple and SandboxConfig.allowed_gpus/max_timeout fields with validator; deduplication and empty-list-to-None normalization are correct.
backend/services/agent/runner.py _format_compute_env now uses resolve_compute_allowance (matching handler enforcement), adds _gpu_hourly_usd for price hints with graceful None fallback; private _resolve_compute_rate import acknowledged in comment.
frontend/src/components/ProjectSettingsModal.tsx Adds Agent GPU allowance checkbox grid with CPU pinned and max-timeout field; GPU_OPTIONS updated to canonical labels (A100→A100-40GB, adds A100-80GB and H100).
frontend/src/lib/types.ts SandboxConfig extended with optional allowed_gpus and max_timeout fields, matching the backend schema.
backend/tests/test_compute_allowance.py Comprehensive tests covering resolver semantics, schema validation, clamp_timeout, and prompt rendering including price-degradation and legacy label cases.
backend/tests/test_execute_code_handler.py Full handler test matrix: allowed GPU passthrough, denial structured errors, precedence rules, timeout clamping, tool_start/tool_end ordering, and owner max_timeout cap on profile fallback.

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]
Loading

Reviews (3): Last reviewed commit: "Merge branch 'staging-v0.0.5' into feat/..." | Re-trigger Greptile

Comment thread backend/skills/execute-code/handler.py
Comment thread backend/services/agent/runner.py
…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
Comment thread backend/services/compute_allowance.py
…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
@lucastononro

Copy link
Copy Markdown
Owner Author

Merged into integration branch staging-v0.0.5 (see the STAGING-v0.0.5.md ledger on that branch for merge order, Greptile follow-ups and test results). These changes will land on main via the staging merge — closing to clear the queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant