Files
AutoGPT/autogpt_platform/backend/backend/copilot/tools/validate_agent.py
Zamil Majdy 7176cecf25 perf(copilot): reduce tool schema token cost by 34% (#12398)
## Summary

Reduce CoPilot per-turn token overhead by systematically trimming tool
descriptions, parameter schemas, and system prompt content. All 35 MCP
tool schemas are passed on every SDK call — this PR reduces their size.

### Strategy

1. **Tool descriptions**: Trimmed verbose multi-sentence explanations to
concise single-sentence summaries while preserving meaning
2. **Parameter schemas**: Shortened parameter descriptions to essential
info, removed some `default` values (handled in code)
3. **System prompt**: Condensed `_SHARED_TOOL_NOTES` and storage
supplement template in `prompting.py`
4. **Cross-tool references**: Removed duplicate workflow hints (e.g.
"call find_block before run_block" appeared in BOTH tools — kept only in
the dependent tool). Critical cross-tool references retained (e.g.
`continue_run_block` in `run_block`, `fix_agent_graph` in
`validate_agent`, `get_doc_page` in `search_docs`, `web_fetch`
preference in `browser_navigate`)

### Token Impact

| Metric | Before | After | Reduction |
|--------|--------|-------|-----------|
| System Prompt | ~865 tokens | ~497 tokens | 43% |
| Tool Schemas | ~9,744 tokens | ~6,470 tokens | 34% |
| **Grand Total** | **~10,609 tokens** | **~6,967 tokens** | **34%** |

Saves **~3,642 tokens per conversation turn**.

### Key Decisions

- **Mostly description changes**: Tool logic, parameters, and types
unchanged. However, some schema-level `default` fields were removed
(e.g. `save` in `customize_agent`) — these are machine-readable
metadata, not just prose, and may affect LLM behavior.
- **Quality preserved**: All descriptions still convey what the tool
does and essential usage patterns
- **Cross-references trimmed carefully**: Kept prerequisite hints in the
dependent tool (run_block mentions find_block) but removed the reverse
(find_block no longer mentions run_block). Critical cross-tool guidance
retained where removal would degrade model behavior.
- **`run_time` description fixed**: Added missing supported values
(today, last 30 days, ISO datetime) per review feedback

### Future Optimization

The SDK passes all 35 tools on every call. The MCP protocol's
`list_tools()` handler supports dynamic tool registration — a follow-up
PR could implement lazy tool loading (register core tools + a discovery
meta-tool) to further reduce per-turn token cost.

### Changes

- Trimmed descriptions across 25 tool files
- Condensed `_SHARED_TOOL_NOTES` and `_build_storage_supplement` in
`prompting.py`
- Fixed `run_time` schema description in `agent_output.py`

### Checklist

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] All 273 copilot tests pass locally
  - [x] All 35 tools load and produce valid schemas
  - [x] Before/after token dumps compared
  - [x] Formatting passes (`poetry run format`)
  - [x] CI green
2026-03-23 08:27:24 +00:00

105 lines
3.3 KiB
Python

"""ValidateAgentGraphTool - Validates agent JSON structure."""
import logging
from typing import Any
from backend.copilot.model import ChatSession
from .agent_generator.validation import AgentValidator, get_blocks_as_dicts
from .base import BaseTool
from .models import ErrorResponse, ToolResponseBase, ValidationResultResponse
logger = logging.getLogger(__name__)
class ValidateAgentGraphTool(BaseTool):
"""Tool for validating agent JSON graphs."""
@property
def name(self) -> str:
return "validate_agent_graph"
@property
def description(self) -> str:
return (
"Validate agent JSON for correctness: block_ids, links, required fields, "
"type compatibility, nested sink notation, prompt brace escaping, "
"and AgentExecutorBlock configs. On failure, use fix_agent_graph to auto-fix."
)
@property
def requires_auth(self) -> bool:
return False
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"agent_json": {
"type": "object",
"description": "Agent JSON with 'nodes' and 'links' arrays.",
},
},
"required": ["agent_json"],
}
async def _execute(
self,
user_id: str | None,
session: ChatSession,
**kwargs,
) -> ToolResponseBase:
agent_json = kwargs.get("agent_json")
session_id = session.session_id if session else None
if not agent_json or not isinstance(agent_json, dict):
return ErrorResponse(
message="Please provide a valid agent JSON object.",
error="Missing or invalid agent_json parameter",
session_id=session_id,
)
nodes = agent_json.get("nodes", [])
if not nodes:
return ErrorResponse(
message="The agent JSON has no nodes. An agent needs at least one block.",
error="empty_agent",
session_id=session_id,
)
try:
blocks = get_blocks_as_dicts()
validator = AgentValidator()
is_valid, error_message = validator.validate(agent_json, blocks)
except Exception as e:
logger.error(f"Validation error: {e}", exc_info=True)
return ErrorResponse(
message=f"Validation encountered an error: {str(e)}",
error="validation_exception",
session_id=session_id,
)
if is_valid:
return ValidationResultResponse(
message="Agent graph is valid! No issues found.",
valid=True,
errors=[],
error_count=0,
session_id=session_id,
)
# Parse individual errors from the validator's error list
errors = validator.errors if hasattr(validator, "errors") else []
if not errors and error_message:
errors = [error_message]
return ValidationResultResponse(
message=f"Found {len(errors)} validation error(s). Fix them and re-validate.",
valid=False,
errors=errors,
error_count=len(errors),
session_id=session_id,
)