feat(agent): typed budget errors, per-step cost tracking, and structred output mode - #631
Open
AseemPrasad wants to merge 1 commit into
Open
feat(agent): typed budget errors, per-step cost tracking, and structred output mode#631AseemPrasad wants to merge 1 commit into
AseemPrasad wants to merge 1 commit into
Conversation
…red output mode Adds three inter-related improvements to the agentic tool-call runtime, all gated behind the new AGENT_STRUCTURED_OUTPUT opt-in flag.
Author
|
@abi |
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.
Three inter-related improvements to the agentic tool-call runtime in
backend/agent/engine.pyandsupporting modules. All changes are fully opt-in via the new
AGENT_STRUCTURED_OUTPUTenvironmentvariable — existing deployments are completely unaffected.
Feature 1: Typed
BudgetExceededErrorProblem
BudgetExceededErrorwas raised with a bare string and re-stringified by the caller, losing the error type.Both "global budget exceeded" and "per-step budget exceeded" looked identical to the frontend.
Solution
BudgetExceededErrorand its new subclassPerStepBudgetExceededErrorcarry two class attributes:Both are caught in PipelineContext._run_variant() and sent as a new budgetExceeded WebSocket message:
Key design decision: unlike variantError, budgetExceeded does not close the session. The user sees a clear
banner and can retry with a different model or settings immediately.
────────────────────────────────────────────────────────────────────────────────
Feature 2: Per-Step Cost Tracking
Problem
Cost was only visible in aggregate at the end of a run (and only if PROMPT_REPORTS_ENABLED=true). There was no
way to observe which step caused a cost spike.
Solution
AgentRunRecorder.record_step_cost() writes a step_cost event to events.jsonl live, and appends a step_costs[]
array to run.json at finalisation:
────────────────────────────────────────────────────────────────────────────────
Feature 3: Structured Output Mode (Opt-In)
Problem
Models with weaker JSON-mode support (especially Gemini family members) occasionally emit malformed JSON tool
calls, causing InvalidJsonToolCallError and retry loops. There was no way to force tool-call mode or to switch
on structured output without a full config refactor.
Solution
New file: backend/agent/modes.py
Config (all in backend/.env, opt-in):
Provider wiring:
┌───────────┬────────────────────┬────────────────────────────────┬──────────────────────────────────────────┐
│ Provider │ FREE │ PREFER_JSON │ FORCE_TOOL │
├───────────┼────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤
│ OpenAI │ tool_choice="auto" │ tool_choice="auto" + JSON mode │ tool_choice="required" │
├───────────┼────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤
│ Anthropic │ no system nudge │ no system nudge │ prepends invisible nudge to system │
│ │ │ │ prompt │
├───────────┼────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤
│ Gemini │ no change │ no change │ no change (see below) │
└───────────┴────────────────────┴────────────────────────────────┴──────────────────────────────────────────┘
│ Note on Gemini: Gemini tool-calling uses forced_function_calling which is all-or-nothing at the API level.
│ Full FORCE_TOOL support requires deeper API changes and is deferred to a future PR.
────────────────────────────────────────────────────────────────────────────────
Files Changed
┌─────────────────────────────────────────┬──────────────────────────────────────────────────────────────────┐
│ File │ Change │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/agent/modes.py │ New — StructuredOutputMode, ToolCallPolicy, mapping dicts │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/config.py │ 4 new env vars with validation + typed annotations │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/ws/constants.py │ BUDGET_EXCEEDED_CODE = 4333 added │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/agent/engine.py │ BudgetExceededError refactored; per-step budget gate; step cost │
│ │ tracking; structured output wiring │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/routes/generate_code.py │ budgetExceeded message type; typed exception handler │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/fs_logging/agent_runs.py │ record_step_cost() method; step_costs[] in run.json │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/agent/providers/factory.py │ structured_output_mode param → provider-native params │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/agent/providers/openai.py │ tool_choice in init + stream_turn │
├─────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ backend/agent/providers/anthropic/provi │ tool_nudge in init + prepended to system prompt │
│ der.py │
Testing Notes
agent/modes.py routes/generate_code.py fs_logging/agent_runs.py)
(frontend/src/lib/ws-messages.ts or equivalent) to surface a distinct UI banner