vet

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

commit 68964d1cabb61caac8417afdd202b56c1ead8240
parent 2dff91a3d23d3f38ad4edac52604028152436007
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date:   Tue, 17 Feb 2026 01:14:57 +0000

Hidden agentic option (#84)

* Added hidden agentic flag

* Added harness specification

* Refactoring agent things

* Isort

* Refactoring + codex model fix

* Bug fix, support codex better

* Default claude code added

---------

Co-authored-by: Andrew Laack <andrew@laack.co>
Diffstat:
Mvet/cli/main.py | 21++++++++++++++++++++-
Mvet/imbue_core/data_types.py | 5+++++
Mvet/imbue_tools/types/vet_config.py | 2++
Mvet/issue_identifiers/agentic_issue_collation.py | 9+++++----
Mvet/issue_identifiers/common.py | 51++++++++++++++++++++++++++++++++++++++++++++-------
Mvet/issue_identifiers/harnesses/agentic.py | 34++++++++++------------------------
6 files changed, 86 insertions(+), 36 deletions(-)

diff --git a/vet/cli/main.py b/vet/cli/main.py @@ -35,6 +35,7 @@ from vet.formatters import issue_to_dict from vet.formatters import validate_output_fields from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError +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 from vet.imbue_tools.get_conversation_history.get_conversation_history import parse_conversation_history @@ -241,6 +242,21 @@ def create_parser() -> argparse.ArgumentParser: help="Suppress progress indicator and non-essential output", ) + parser.add_argument( + "--agentic", + action="store_true", + default=False, + help=argparse.SUPPRESS, + ) + + parser.add_argument( + "--agent-harness", + type=AgentHarnessType, + choices=list(AgentHarnessType), + default=AgentHarnessType.CLAUDE, + help=argparse.SUPPRESS, + ) + return parser @@ -492,8 +508,10 @@ def main(argv: list[str] | None = None) -> int: language_model_config = build_language_model_config(model_id, user_config) max_output_tokens = get_max_output_tokens_for_model(model_id, user_config) + disabled_identifiers = None if args.agentic else ("agentic_issue_identifier",) + config = VetConfig( - disabled_identifiers=("agentic_issue_identifier",), + disabled_identifiers=disabled_identifiers, language_model_generation_config=language_model_config, enabled_issue_codes=(tuple(args.enabled_issue_codes) if args.enabled_issue_codes else None), disabled_issue_codes=(tuple(args.disabled_issue_codes) if args.disabled_issue_codes else None), @@ -503,6 +521,7 @@ def main(argv: list[str] | None = None) -> int: max_output_tokens=max_output_tokens or 20000, max_identifier_spend_dollars=args.max_spend, custom_guides_config=custom_guides_config, + agent_harness_type=args.agent_harness, ) try: diff --git a/vet/imbue_core/data_types.py b/vet/imbue_core/data_types.py @@ -167,6 +167,11 @@ class AgenticPhase(StrEnum): DEDUPLICATION = "deduplication" +class AgentHarnessType(StrEnum): + CLAUDE = "claude" + CODEX = "codex" + + class IssueIdentifierType(StrEnum): BATCHED_COMMIT_CHECK = "batched_commit_check" CORRECTNESS_COMMIT_CLASSIFIER = "correctness_commit_classifier" diff --git a/vet/imbue_tools/types/vet_config.py b/vet/imbue_tools/types/vet_config.py @@ -2,6 +2,7 @@ from pathlib import Path from vet.imbue_core.agents.configs import LanguageModelGenerationConfig from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName +from vet.imbue_core.data_types import AgentHarnessType from vet.imbue_core.data_types import CustomGuidesConfig from vet.imbue_core.data_types import IssueCode from vet.imbue_core.data_types import get_valid_issue_code_values @@ -35,6 +36,7 @@ class VetConfig(SerializableModel): max_identifier_spend_dollars: float | None = None max_output_tokens: int = 20000 enable_parallel_agentic_issue_identification: bool = False + agent_harness_type: AgentHarnessType = AgentHarnessType.CLAUDE max_identify_workers: int | None = None temperature: float = 0.5 diff --git a/vet/issue_identifiers/agentic_issue_collation.py b/vet/issue_identifiers/agentic_issue_collation.py @@ -20,8 +20,8 @@ from vet.issue_identifiers.common import GeneratedResponseSchema from vet.issue_identifiers.common import extract_invocation_info_from_messages from vet.issue_identifiers.common import format_issue_identification_guide_for_llm from vet.issue_identifiers.common import generate_issues_from_response_texts -from vet.issue_identifiers.common import generate_response_from_claude_code -from vet.issue_identifiers.common import get_claude_code_options +from vet.issue_identifiers.common import generate_response_from_agent +from vet.issue_identifiers.common import get_agent_options from vet.issue_identifiers.identification_guides import IssueIdentificationGuide from vet.issue_identifiers.utils import ReturnCapturingGenerator @@ -145,9 +145,10 @@ def collate_issues_with_agent( all_issues.append(issue) issue_generator_debug_info = issue_generator_with_capture.return_value - options = get_claude_code_options( + options = get_agent_options( cwd=project_context.repo_path, model_name=config.language_model_generation_config.model_name, + agent_harness_type=config.agent_harness_type, ) combined_issues_string = _convert_parsed_issues_to_combined_string(all_issues) collation_prompt = _get_collation_prompt( @@ -157,7 +158,7 @@ def collate_issues_with_agent( combined_issues_string, guides_by_issue_code, ) - claude_response = generate_response_from_claude_code(collation_prompt, options) + claude_response = generate_response_from_agent(collation_prompt, options) assert claude_response is not None response_text, collation_messages = claude_response collation_raw_messages = tuple(json.dumps(message.model_dump()) for message in collation_messages) diff --git a/vet/issue_identifiers/common.py b/vet/issue_identifiers/common.py @@ -13,15 +13,20 @@ from pydantic import PrivateAttr from vet.imbue_core.agents.agent_api.api import get_agent_client from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions +from vet.imbue_core.agents.agent_api.codex.data_types import CodexOptions from vet.imbue_core.agents.agent_api.data_types import AgentAssistantMessage from vet.imbue_core.agents.agent_api.data_types import AgentMessage +from vet.imbue_core.agents.agent_api.data_types import AgentOptions from vet.imbue_core.agents.agent_api.data_types import AgentResultMessage 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.llm_apis.anthropic_api import AnthropicModelName 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.agents.llm_apis.openai_api import OpenAIModelName from vet.imbue_core.async_monkey_patches import log_exception +from vet.imbue_core.data_types import AgentHarnessType from vet.imbue_core.data_types import ConfidenceScore from vet.imbue_core.data_types import IdentifiedVerifyIssue from vet.imbue_core.data_types import InvocationInfo @@ -189,19 +194,51 @@ def convert_to_issue_identifier_result( return generator_with_capture.return_value -def get_claude_code_options(cwd: Path | None, model_name: str) -> ClaudeCodeOptions: - options = ClaudeCodeOptions( +_ANTHROPIC_MODEL_NAMES = {m.value for m in AnthropicModelName} +_OPENAI_MODEL_NAMES = {m.value for m in OpenAIModelName} +_DEFAULT_CODEX_MODEL = "gpt-5.2-codex" +_DEFAULT_CLAUDE_MODEL = AnthropicModelName.CLAUDE_4_6_OPUS + + +def get_agent_options(cwd: Path | None, model_name: str, agent_harness_type: AgentHarnessType) -> AgentOptions: + # NOTE: This if/else is intentionally simple. We're unlikely to support many harness types, + # but if we do, this should be refactored into a registry or factory pattern. + if agent_harness_type == AgentHarnessType.CODEX: + if model_name in _ANTHROPIC_MODEL_NAMES: + logger.info( + "Config model_name {config_model_name} is an Anthropic model, using default Codex model ({model_name}).", + config_model_name=model_name, + model_name=_DEFAULT_CODEX_MODEL, + ) + model_name = _DEFAULT_CODEX_MODEL + return CodexOptions( + cwd=cwd, + model=model_name, + sandbox_mode="read-only", + ) + if model_name in _OPENAI_MODEL_NAMES: + logger.info( + "Config model_name {config_model_name} is an OpenAI model, using default Claude model ({model_name}).", + config_model_name=model_name, + model_name=_DEFAULT_CLAUDE_MODEL, + ) + model_name = _DEFAULT_CLAUDE_MODEL + elif model_name not in _ANTHROPIC_MODEL_NAMES: + logger.info( + "Config model_name {config_model_name} is not a valid Anthropic model, using default ({model_name}).", + config_model_name=model_name, + model_name=_DEFAULT_CLAUDE_MODEL, + ) + model_name = _DEFAULT_CLAUDE_MODEL + return ClaudeCodeOptions( cwd=cwd, permission_mode="bypassPermissions", # Equivalent to --dangerously-skip-permissions allowed_tools=list(READ_ONLY_TOOLS) + [AgentToolName.BASH], model=model_name, ) - return options -def generate_response_from_claude_code( - prompt: str, options: ClaudeCodeOptions -) -> tuple[str, list[AgentMessage]] | None: +def generate_response_from_agent(prompt: str, options: AgentOptions) -> tuple[str, list[AgentMessage]] | None: messages = [] assistant_messages = [] result_message = None @@ -214,7 +251,7 @@ def generate_response_from_claude_code( elif isinstance(message, AgentResultMessage): result_message = message except Exception as e: - log_exception(e, "Claude Code API call failed") + log_exception(e, "Agent API call failed") return None # Try to get response from result message first diff --git a/vet/issue_identifiers/harnesses/agentic.py b/vet/issue_identifiers/harnesses/agentic.py @@ -1,5 +1,5 @@ """ -Agentic harness that checks a given diff for issues using Claude Code agents with tools. +Agentic harness that checks a given diff for issues using coding agents with tools. """ import concurrent.futures @@ -12,11 +12,8 @@ from typing import Generator import jinja2 from loguru import logger -from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions from vet.imbue_core.agents.agent_api.data_types import AgentMessage -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.llm_apis.anthropic_api import AnthropicModelName +from vet.imbue_core.agents.agent_api.data_types import AgentOptions from vet.imbue_core.async_monkey_patches import log_exception from vet.imbue_core.data_types import AgenticPhase from vet.imbue_core.data_types import IssueCode @@ -33,7 +30,8 @@ from vet.issue_identifiers.common import GeneratedResponseSchema from vet.issue_identifiers.common import extract_invocation_info_from_messages from vet.issue_identifiers.common import format_issue_identification_guide_for_llm from vet.issue_identifiers.common import generate_issues_from_response_texts -from vet.issue_identifiers.common import generate_response_from_claude_code +from vet.issue_identifiers.common import generate_response_from_agent +from vet.issue_identifiers.common import get_agent_options from vet.issue_identifiers.harnesses.base import IssueIdentifierHarness from vet.issue_identifiers.identification_guides import IssueIdentificationGuide @@ -169,9 +167,9 @@ ResponseText = str def _generate_issues_worker( issue_code: IssueCode, prompt: str, - options: ClaudeCodeOptions, + options: AgentOptions, ) -> tuple[IssueCode, ResponseText, list[AgentMessage]] | None: - issue_result = generate_response_from_claude_code(prompt, options) + issue_result = generate_response_from_agent(prompt, options) if issue_result is None: return None return issue_code, issue_result[0], issue_result[1] @@ -247,22 +245,10 @@ class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): ) -> Generator[GeneratedIssueSchema, None, IssueIdentificationDebugInfo]: assert project_context.repo_path is not None, "Project context must have a valid repo_path, got None" - config_model_name = config.language_model_generation_config.model_name - if config_model_name in [anthropic_model.value for anthropic_model in AnthropicModelName]: - model_name = config_model_name - else: - model_name = AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01 - logger.info( - "Config model_name {config_model_name} is not a valid Anthropic model, using default ({model_name}).", - config_model_name=config_model_name, - model_name=model_name, - ) - - options = ClaudeCodeOptions( + options = get_agent_options( cwd=project_context.repo_path, - permission_mode="bypassPermissions", # Equivalent to --dangerously-skip-permissions - allowed_tools=list(READ_ONLY_TOOLS) + [AgentToolName.BASH], # Allow read-only tools for analysis - model=model_name, + model_name=config.language_model_generation_config.model_name, + agent_harness_type=config.agent_harness_type, ) if config.enable_parallel_agentic_issue_identification: @@ -312,7 +298,7 @@ class _AgenticIssueIdentifier(IssueIdentifier[CommitInputs]): return IssueIdentificationDebugInfo(llm_responses=tuple(llm_responses)) else: prompt = self._get_prompt(project_context, config, identifier_inputs) - claude_response = generate_response_from_claude_code(prompt, options) + claude_response = generate_response_from_agent(prompt, options) assert claude_response is not None response_text, messages = claude_response