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:
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)