vet

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 2571a790c3463a178e73000ba121d6fd9b33aac3
parent 1c9cc8b91235e3334f2324112d0178e23e6c985f
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date:   Thu, 26 Feb 2026 11:18:16 -0600

Better exception messaging (#160)

* Better exception messaging

* Better handling

* Inform about -vv in cases we are uncertain about.

---------

Co-authored-by: Andrew Laack <andrew@laack.co>
Diffstat:
Mvet/cli/main.py | 20++++++++++++++++++--
Mvet/issue_identifiers/agentic_issue_collation.py | 7+------
Mvet/issue_identifiers/common.py | 16+++++++++++-----
Mvet/issue_identifiers/harnesses/agentic.py | 27++++++++++++---------------
4 files changed, 42 insertions(+), 28 deletions(-)

diff --git a/vet/cli/main.py b/vet/cli/main.py @@ -26,6 +26,7 @@ from vet.formatters import OUTPUT_FIELDS from vet.formatters import OUTPUT_FORMATS from vet.formatters import validate_output_fields from vet.imbue_core.agents.agent_api.errors import AgentCLINotFoundError +from vet.imbue_core.agents.agent_api.errors import AgentProcessError from vet.imbue_core.data_types import AgentHarnessType from vet.imbue_core.data_types import IssueCode from vet.imbue_core.data_types import get_valid_issue_code_values @@ -408,15 +409,16 @@ _CONTEXT_OVERFLOW_PATTERNS = [ "maximum context length", "too many tokens", "reduce the length of the messages", + "ran out of room in the model's context window", ] -def _is_context_overflow(e) -> bool: +def _is_context_overflow(e: Exception) -> bool: from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError if isinstance(e, PromptTooLongError): return True - error_msg = e.error_message.lower() + error_msg = getattr(e, "error_message", str(e)).lower() return any(pattern in error_msg for pattern in _CONTEXT_OVERFLOW_PATTERNS) @@ -530,6 +532,7 @@ def main(argv: list[str] | None = None) -> int: from vet.formatters import format_issue_text from vet.formatters import issue_to_dict from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError + from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError from vet.imbue_tools.types.vet_config import VetConfig @@ -632,6 +635,19 @@ def main(argv: list[str] | None = None) -> int: except AgentCLINotFoundError as e: print(f"vet: {e}", file=sys.stderr) return 2 + except AgentProcessError as e: + if _is_context_overflow(e): + print( + "vet: review failed because too much context was provided to the model. " + "Consider using a model with a larger context window, or a narrower base commit.", + file=sys.stderr, + ) + return 2 + print(f"vet: {e}\nRe-run with -vv for more details.", file=sys.stderr) + return 1 + except MissingAPIKeyError as e: + print(f"vet: {e}", file=sys.stderr) + return 2 # TODO: This should be refactored so we only need to handle prompt too long errors when context is overfilled. except (PromptTooLongError, BadAPIRequestError) as e: if _is_context_overflow(e): diff --git a/vet/issue_identifiers/agentic_issue_collation.py b/vet/issue_identifiers/agentic_issue_collation.py @@ -158,12 +158,7 @@ def collate_issues_with_agent( combined_issues_string, guides_by_issue_code, ) - agent_response = generate_response_from_agent(collation_prompt, options) - if agent_response is None: - raise RuntimeError( - "Agentic issue collation failed: no response received from agent CLI." " Re-run with --verbose for details." - ) - response_text, collation_messages = agent_response + response_text, collation_messages = generate_response_from_agent(collation_prompt, options) collation_raw_messages = tuple(json.dumps(message.model_dump()) for message in collation_messages) collation_invocation_info = extract_invocation_info_from_messages(collation_messages) diff --git a/vet/issue_identifiers/common.py b/vet/issue_identifiers/common.py @@ -22,6 +22,7 @@ from vet.imbue_core.agents.agent_api.data_types import AgentTextBlock from vet.imbue_core.agents.agent_api.data_types import AgentToolName from vet.imbue_core.agents.agent_api.data_types import READ_ONLY_TOOLS from vet.imbue_core.agents.agent_api.errors import AgentCLINotFoundError +from vet.imbue_core.agents.agent_api.errors import AgentProcessError from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse from vet.imbue_core.async_monkey_patches import log_exception @@ -217,7 +218,7 @@ def get_agent_options(cwd: Path | None, model_name: str | None, agent_harness_ty ) -def generate_response_from_agent(prompt: str, options: AgentOptions) -> tuple[str, list[AgentMessage]] | None: +def generate_response_from_agent(prompt: str, options: AgentOptions) -> tuple[str, list[AgentMessage]]: messages = [] assistant_messages = [] result_message = None @@ -231,14 +232,19 @@ def generate_response_from_agent(prompt: str, options: AgentOptions) -> tuple[st result_message = message except AgentCLINotFoundError: raise + except AgentProcessError: + # If the agent reported an error before the process failed, use that detail instead — + # it typically contains a more specific message (e.g. "ran out of room in context window"). + if result_message and result_message.is_error: + error_detail = result_message.error or result_message.result or "unknown error" + raise AgentProcessError(f"Agent CLI returned an error: {error_detail}") from None + raise except Exception as e: - log_exception(e, "Agent API call failed") - return None + raise AgentProcessError(f"Agent CLI call failed: {e}") from e if result_message and result_message.is_error: error_detail = result_message.error or result_message.result or "unknown error" - logger.error("Agent CLI returned an error: {error_detail}", error_detail=error_detail) - return None + raise AgentProcessError(f"Agent CLI returned an error: {error_detail}") # Try to get response from result message first response_text = "" diff --git a/vet/issue_identifiers/harnesses/agentic.py b/vet/issue_identifiers/harnesses/agentic.py @@ -168,11 +168,9 @@ def _generate_issues_worker( issue_code: IssueCode, prompt: str, options: AgentOptions, -) -> tuple[IssueCode, ResponseText, list[AgentMessage]] | None: - issue_result = generate_response_from_agent(prompt, options) - if issue_result is None: - return None - return issue_code, issue_result[0], issue_result[1] +) -> tuple[IssueCode, ResponseText, list[AgentMessage]]: + response_text, agent_messages = generate_response_from_agent(prompt, options) + return issue_code, response_text, agent_messages class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): @@ -267,6 +265,8 @@ class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): for issue_code, prompt in issue_prompts ] + num_succeeded = 0 + last_error: Exception | None = None for task in concurrent.futures.as_completed(tasks): try: result = task.result() @@ -274,11 +274,10 @@ class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): raise except Exception as e: log_exception(e, "Error processing issue type: {e}", e=e) + last_error = e continue - if result is None: - continue - + num_succeeded += 1 issue_code, issue_type_response_text, messages = result yield from generate_issues_from_response_texts(response_texts=(issue_type_response_text,)) @@ -297,16 +296,14 @@ class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): ) ) + # If every task failed, re-raise the last error so it propagates to main(). + if num_succeeded == 0 and last_error is not None: + raise last_error + return IssueIdentificationDebugInfo(llm_responses=tuple(llm_responses)) else: prompt = self._get_prompt(project_context, config, identifier_inputs) - agent_response = generate_response_from_agent(prompt, options) - if agent_response is None: - raise RuntimeError( - "Agentic issue identification failed: no response received from agent CLI." - " Re-run with --verbose for details." - ) - response_text, messages = agent_response + response_text, messages = generate_response_from_agent(prompt, options) message_dumps = tuple(json.dumps(message.model_dump()) for message in messages) invocation_info = extract_invocation_info_from_messages(messages)