diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index ebeb06fc07..2a4fa028a0 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import re -from dataclasses import dataclass +from dataclasses import dataclass, fields from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, Literal, Optional @@ -25,13 +25,9 @@ _INCLUDED_NOTE = "subscription-included; no provider invoice for usage" def format_cost_label(amount: Decimal) -> str: - """Format a cost as a display label, scaling precision to magnitude. - - Zero → "$0.00"; sub-cent → "~$0.0046" (4 dp, or "~$<0.0001" when the - amount rounds to 0.0000 so the label never reads as zero); else "~$1.23". - Shared by per-response cost labels and the insights cost-bucket - formatters so sub-cent honesty cannot regress on one surface. - """ + """Cost display label: zero → "$0.00"; sub-cent → "~$0.0046" (4 dp, or + "~$<0.0001" when it rounds to 0.0000 so the label never reads as zero); + else "~$1.23". Shared by per-response labels and insights cost buckets.""" if amount == _ZERO: return "$0.00" if amount < _SUBCENT_THRESHOLD: @@ -72,14 +68,10 @@ class CanonicalUsage: combined figure covers.""" if not isinstance(other, CanonicalUsage): return NotImplemented - return CanonicalUsage( - input_tokens=self.input_tokens + other.input_tokens, - output_tokens=self.output_tokens + other.output_tokens, - cache_read_tokens=self.cache_read_tokens + other.cache_read_tokens, - cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens, - reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens, - request_count=self.request_count + other.request_count, raw_usage=None, - ) + return CanonicalUsage(**{ + f.name: getattr(self, f.name) + getattr(other, f.name) + for f in fields(CanonicalUsage) if f.name != "raw_usage" + }) @dataclass(frozen=True) diff --git a/agent/verification_evidence.py b/agent/verification_evidence.py index a73f0801fa..d7a7147d20 100644 --- a/agent/verification_evidence.py +++ b/agent/verification_evidence.py @@ -1,9 +1,6 @@ -"""Coding verification evidence ledger. - -Records what the agent actually proved while working in a code workspace. It is -deliberately passive: it never decides to run a suite, never blocks completion, -and never upgrades targeted checks into "repo green". -""" +"""Coding verification evidence ledger: records what the agent actually proved in +a code workspace. Deliberately passive — it never runs a suite, never blocks +completion, and never upgrades targeted checks into "repo green".""" from __future__ import annotations diff --git a/agent/verification_stop.py b/agent/verification_stop.py index 9ebc000a09..9ad54f7f87 100644 --- a/agent/verification_stop.py +++ b/agent/verification_stop.py @@ -1,9 +1,6 @@ -"""Turn-end verification guard for coding edits. - -Policy-only: it never runs checks itself. It turns the passive verification -ledger into a bounded follow-up when the model tries to finish right after -editing code without fresh evidence. -""" +"""Turn-end verification guard for coding edits. Policy-only: it never runs +checks itself, it turns the passive verification ledger into a bounded follow-up +when the model tries to finish right after editing code without fresh evidence.""" from __future__ import annotations @@ -98,10 +95,8 @@ def _candidate_cwds(paths: Iterable[str]) -> list[Path]: def _verification_snapshot( *, session_id: str | None, changed_paths: list[str] ) -> tuple[dict[str, Any], dict[str, Any]] | None: - """Return ``(status, facts)`` for the first edited workspace needing proof. - - Falls back to the first recognized workspace when every one is ``passed``. - """ + """``(status, facts)`` for the first edited workspace needing proof, else the + first recognized workspace when every one is ``passed``.""" try: from agent.coding_context import project_facts_for from agent.verification_evidence import verification_status diff --git a/agent/verify/recipes.py b/agent/verify/recipes.py index d7f90440ba..52d0b42353 100644 --- a/agent/verify/recipes.py +++ b/agent/verify/recipes.py @@ -25,12 +25,9 @@ def _as_strings(value: Any) -> list[str]: @dataclass class Recipe: - """A runnable verification recipe for a project. - - Mirrors grok-cli's ``VerifyRecipe`` (scoped): ``name`` is the human label - (``appLabel``), ``kind`` the detector id (``appKind``); command lists are - shell strings executed in the project root. - """ + """A runnable verification recipe (grok-cli's ``VerifyRecipe``): ``name`` is the + human label (``appLabel``), ``kind`` the detector id (``appKind``); command + lists are shell strings executed in the project root.""" name: str kind: str = "unknown" @@ -287,12 +284,8 @@ def _detect_compose_recipe(root: Path) -> Recipe | None: def detect_recipe(root: Path) -> Recipe | None: - """Detect a verification recipe for the project at ``root``. - - Order mirrors grok-cli's ``inferFallbackRecipe``: package.json wins, then - Python, Go, Rust, Java, then Makefile / docker-compose fallbacks. ``None`` - when nothing recognizable is found. - """ + """Detect a recipe for ``root`` (grok-cli's ``inferFallbackRecipe`` order): package.json + wins, then Python, Go, Rust, Java, then Makefile / docker-compose; ``None`` if nothing matches.""" root = Path(root) pkg = _read_package_json(root) if pkg is not None: diff --git a/agent/verify/runner.py b/agent/verify/runner.py index bf2963e5f8..054f573bd6 100644 --- a/agent/verify/runner.py +++ b/agent/verify/runner.py @@ -1,12 +1,9 @@ -"""Verification runner: execute a Recipe's phases and smoke-test the app. +"""Verification runner: bootstrap -> build -> test -> start in background -> +readiness loop -> teardown (scoped port of grok-cli's verify flow). -Scoped port of grok-cli's verify sub-agent flow (bootstrap -> build -> test -> -start in background -> readiness loop -> teardown) as a plain subprocess runner. - -Commands come from the project's own recipe (package.json scripts, Makefile -targets, ...) and run with ``shell=True`` on purpose: this is a developer tool -running the project's own build commands in its own checkout — the same trust -level as the terminal tool. +Commands come from the project's own recipe and run with ``shell=True`` on +purpose: a developer tool running the project's own build commands in its own +checkout — the same trust level as the terminal tool. """ from __future__ import annotations @@ -128,12 +125,8 @@ def _poll_readiness(url: str, timeout: float, interval: float = 1.0) -> tuple[bo def _terminate_process_group(proc: subprocess.Popen) -> None: - """Terminate the started app and its whole process group cleanly. - - On POSIX the child is spawned with ``start_new_session=True`` so we can - signal the whole group; on Windows (no ``os.killpg``) we fall back to - terminating just the direct child. SIGTERM first, SIGKILL after 10s. - """ + """SIGTERM the app's process group (``start_new_session=True`` on POSIX; just the + direct child on Windows, which lacks ``os.killpg``), SIGKILL after 10s.""" if proc.poll() is not None: return killpg = getattr(os, "killpg", None) @@ -195,12 +188,8 @@ def run_verify( skip_start: bool = False, port_override: int | None = None, stop_on_failure: bool = True, on_output: Callable[[str], None] | None = None, ) -> VerifyResult: - """Run a verify pass for ``recipe`` at project ``root``. - - Executes the selected command phases sequentially, then (unless - ``skip_start`` or a phase failed) launches ``recipe.start`` in the - background, polls the readiness URL, and tears the process group down. - """ + """Run the selected command phases sequentially, then (unless ``skip_start`` or a + phase failed) boot ``recipe.start``, poll readiness, and tear the process group down.""" root = Path(root) selected = tuple(phases) if phases else PHASE_ORDER + ("start",) result = VerifyResult(recipe_name=recipe.name) diff --git a/agent/video_gen_provider.py b/agent/video_gen_provider.py index 94d3880a25..14b0092286 100644 --- a/agent/video_gen_provider.py +++ b/agent/video_gen_provider.py @@ -38,23 +38,14 @@ DEFAULT_RESOLUTION = "720p" class VideoGenProvider(CatalogProviderBase): - """Abstract base class for a video generation backend. - - Subclasses must implement :attr:`name` and :meth:`generate`; everything else - has defaults. ``list_models`` entries are **model families** and may add - ``speed`` / ``strengths`` / ``price`` / advisory ``modalities``. - """ + """Abstract base class for a video generation backend: implement :attr:`name` + and :meth:`generate`. ``list_models`` entries are **model families** and may + add ``speed`` / ``strengths`` / ``price`` / advisory ``modalities``.""" def capabilities(self) -> Dict[str, Any]: - """What this provider supports (all keys optional): ``modalities``, - ``aspect_ratios``, ``resolutions``, ``max_duration`` / ``min_duration``, - ``supports_audio`` / ``supports_negative_prompt`` / ``supports_seed`` / - ``supports_upscale``, ``max_reference_images``. - - Used for soft validation, capability-gated params in the dynamic - ``video_generate`` schema (args a backend can't honor aren't advertised), - and the picker. Default fails closed: text-only, no optional features. - """ + """Supported features (keys below, all optional) used for soft validation, + capability-gated params in the dynamic ``video_generate`` schema, and the + picker. Default fails closed: text-only, no optional features.""" return { "modalities": ["text"], "aspect_ratios": list(COMMON_ASPECT_RATIOS), "resolutions": list(COMMON_RESOLUTIONS), "max_duration": 10, "min_duration": 1, @@ -70,13 +61,10 @@ class VideoGenProvider(CatalogProviderBase): negative_prompt: Optional[str] = None, audio: Optional[bool] = None, seed: Optional[int] = None, **kwargs: Any, ) -> Dict[str, Any]: - """Generate a video from a prompt, or animate ``image_url`` when given. - - Return :func:`success_response` / :func:`error_response`. Unknown - ``kwargs`` MUST be ignored (forward compat). Known optional kwarg: - ``upscale`` (bool) — a post-generation high-res pass; providers that - honor it report ``upscaled: True`` in ``extra``. - """ + """Generate a video from a prompt, or animate ``image_url`` when given; return + :func:`success_response` / :func:`error_response`. Unknown ``kwargs`` MUST be + ignored. Known optional kwarg ``upscale`` (bool): a post-generation high-res + pass; providers that honor it report ``upscaled: True`` in ``extra``.""" def save_b64_video(b64_data: str,*, prefix: str="video", extension: str="mp4") -> Path: @@ -97,11 +85,8 @@ _URL_VIDEO_CONTENT_TYPES = { def save_url_video( url: str, *, prefix: str = "video", timeout: float = 180.0, max_bytes: int = 200 * 1024 * 1024 ) -> Path: - """Download an (often ephemeral) video URL into ``$HERMES_HOME/cache/videos/``. - - Raises on network / HTTP / oversize / empty errors so callers can fall back - to returning the bare URL. See :mod:`agent.provider_media`. - """ + """Download an (often ephemeral) video URL into ``$HERMES_HOME/cache/videos/``; + raises on network / HTTP / oversize / empty errors so callers can fall back to the URL.""" return provider_media.save_url( "videos", url, prefix=prefix, timeout=timeout, max_bytes=max_bytes, chunk_size=256 * 1024, content_types=_URL_VIDEO_CONTENT_TYPES, @@ -139,19 +124,10 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): """Generic text/image-to-video over the OpenAI ``client.videos`` API. DeepInfra, OpenAI/Sora, and OpenRouter share the ``POST /videos`` async-job - shape (``create`` → poll → ``download_content``), so the SDK call lives here - once; a concrete backend declares identity and credentials:: - - class FooVideoGenProvider(OpenAICompatibleVideoGenProvider): - name = "foo" - _env_key = "FOO_API_KEY" - _default_base_url = "https://api.foo.com/v1/openai" - def list_models(self): - return [...] # entries with an "id" key; default_model() uses [0] - - ``image_url`` routes to image-to-video; its absence routes to text-to-video. - Provider-specific fields (``image_url``/``negative_prompt``/``seed``) ride - in ``extra_body`` so they pass through the SDK unchanged. + shape (``create`` → poll → ``download_content``); a concrete backend sets + ``name``, ``_env_key``, ``_default_base_url`` and ``list_models()`` (entries + with an ``id`` key; ``default_model()`` uses ``[0]``). Provider-specific + fields (``image_url``/``negative_prompt``/``seed``) ride in ``extra_body``. """ _env_key: str = "OPENAI_API_KEY" diff --git a/agent/vision_message_prep.py b/agent/vision_message_prep.py index ac437eff36..c109efe443 100644 --- a/agent/vision_message_prep.py +++ b/agent/vision_message_prep.py @@ -115,12 +115,8 @@ class VisionMessagePrepMixin: return note def _model_supports_vision(self) -> bool: - """Return True if the active provider+model reports native vision. - - Resolution: ``model.supports_vision`` > ``providers.

.models..supports_vision`` > models.dev - lookup (see ``image_routing._supports_vision_override``). Custom/local models absent from models.dev - would otherwise be misclassified and have their images stripped. - """ + """True if the active provider+model reports native vision (config override + > models.dev; see ``image_routing._supports_vision_override``).""" try: from hermes_cli.config import load_config from agent.image_routing import _lookup_supports_vision @@ -131,11 +127,8 @@ class VisionMessagePrepMixin: return False def _provider_supports_vision_tool_messages(self) -> bool: - """Return True if the active provider accepts list-type tool content. - - Some providers (Xiaomi MiMo) accept multimodal user messages but 400 on list-type tool content; - reads the provider profile's ``supports_vision_tool_messages``. - """ + """True if the active provider accepts list-type tool content (some, e.g. Xiaomi MiMo, take + multimodal user messages but 400 on list-type tool content; profile ``supports_vision_tool_messages``).""" try: from providers import get_provider_profile profile = get_provider_profile((getattr(self, "provider", "") or "").strip()) @@ -190,11 +183,8 @@ class VisionMessagePrepMixin: return cache[mode] def _prepare_messages_for_non_vision_model(self, api_messages: list) -> list: - """Replace native image parts with cached vision_analyze text when the active model lacks vision. - - Vision-capable models pass through unchanged (the provider adapter — including the Anthropic one — - handles image parts natively). The text fallback is the historically Anthropic-named preprocessor. - """ + """Replace native image parts with cached vision_analyze text when the active model lacks vision; + vision-capable models pass through unchanged (the provider adapter handles image parts natively).""" if not any( isinstance(msg, dict) and self._content_has_image_parts(msg.get("content")) for msg in api_messages ) or self._model_supports_vision(): @@ -212,11 +202,8 @@ class VisionMessagePrepMixin: _prepare_anthropic_messages_for_api = _prepare_messages_for_non_vision_model def _tool_result_content_for_active_model(self, tool_name: str, result: Any) -> Any: - """Return the tool message content that is safe for the active model. - - Text-only providers must not receive image parts: a rejected tool result becomes canonical history - and can make the next user turn fail before the agent can recover. - """ + """Tool message content that is safe for the active model. Text-only providers must not receive + image parts: a rejected tool result becomes canonical history and can break the next user turn.""" if not _is_multimodal_tool_result(result): return result @@ -269,10 +256,10 @@ class VisionMessagePrepMixin: def _try_strip_image_parts_from_tool_messages( self, api_messages: list, *, remember_model: bool = True ) -> bool: - """Downgrade list-type tool messages to text summaries in place; returns True if any were downgraded. + """Downgrade list-type tool messages to text in place; True if any were downgraded. Recovery for providers that 400 on list-type tool content (e.g. MiMo "text is not set"). By default - records the (provider, model) in ``_no_list_tool_content_models`` so later results downgrade without a + records (provider, model) in ``_no_list_tool_content_models`` so later results downgrade without a round-trip; 413 recovery passes ``remember_model=False`` (body too large ≠ provider rejects lists). """ if not isinstance(api_messages, list): @@ -316,11 +303,8 @@ class VisionMessagePrepMixin: return changed def _anthropic_preserve_dots(self) -> bool: - """True when using an anthropic-compatible endpoint that preserves dots in model names. - - DashScope, MiniMax, Xiaomi MiMo, OpenCode Go/Zen (non-Claude), ZAI/Zhipu keep dots; AWS Bedrock uses - dotted inference-profile IDs and rejects the hyphenated form with HTTP 400. - """ + """True for anthropic-compatible endpoints that keep dots in model names (DashScope, MiniMax, Xiaomi + MiMo, OpenCode Go/Zen, ZAI/Zhipu; Bedrock's dotted inference-profile IDs 400 on the hyphenated form).""" if (getattr(self, "provider", "") or "").lower() in { "alibaba", "minimax", "minimax-cn", "opencode-go", "opencode-zen", "zai", "bedrock", "xiaomi", "vertex", }: diff --git a/agent/web_search_provider.py b/agent/web_search_provider.py index 389cba3ca9..656d927734 100644 --- a/agent/web_search_provider.py +++ b/agent/web_search_provider.py @@ -22,13 +22,9 @@ from agent.provider_base import ProviderBase def get_provider_env(name: str) -> str: - """Config-aware env lookup: ``os.environ`` first, then ``~/.hermes/.env``. - - Credentials set through Hermes' config layer must be visible even when never - exported into the process environment (gateway sessions, delegate children, - subprocess agent runs). Falls back to bare ``os.getenv`` when the config - module is unavailable. Returns the stripped value, or ``""`` when unset. - """ + """Config-aware env lookup (``os.environ`` first, then ``~/.hermes/.env``) so + credentials set through the config layer are visible in gateway sessions / + delegate children / subprocess runs. Stripped value, or ``""`` when unset.""" try: from hermes_cli.config import get_env_value @@ -41,36 +37,23 @@ def get_provider_env(name: str) -> str: class WebSearchProvider(ProviderBase): - """Abstract base class for a web search/extract backend. - - Subclasses implement :meth:`is_available` and at least one of :meth:`search` - / :meth:`extract`; the :meth:`supports_search` / :meth:`supports_extract` - flags let the registry route each capability, so one class can serve both. - """ + """Abstract base class for a web search/extract backend: implement :meth:`is_available` + and at least one of :meth:`search` / :meth:`extract`; the ``supports_*`` flags route each capability.""" @abc.abstractmethod def is_available(self) -> bool: - """True when this provider can service calls. - - Cheap check only (env var present, dep importable, instance URL set) — - must NOT make network calls; runs at tool-registration time and on every - ``hermes tools`` paint. - """ + """True when this provider can service calls. Cheap check only (env var, importable + dep, instance URL) — NO network; runs at tool registration and on every ``hermes tools`` paint.""" def supports_search(self) -> bool: """True if this provider implements :meth:`search`.""" return True def is_keyless_available(self) -> bool: - """True when this provider can serve calls WITHOUT credentials. - - A weaker tier than :meth:`is_available`, used only when NO provider is - configured or keyed (public anonymous free tiers such as Exa / Parallel - MCP). It must never make :meth:`is_available` True, or the legacy - preference walk would route users holding real credentials for a - lower-priority backend onto a higher-priority backend's free tier. - Cheap, no network. Default False. - """ + """True when this provider can serve calls WITHOUT credentials (public anonymous + free tiers such as Exa / Parallel MCP); used only when NO provider is configured or + keyed. Must never make :meth:`is_available` True, or the legacy preference walk would + route keyed users onto a higher-priority backend's free tier. Cheap, no network.""" return False def supports_extract(self) -> bool: @@ -85,13 +68,9 @@ class WebSearchProvider(ProviderBase): ) def extract(self, urls: List[str], **kwargs: Any) -> Any: - """Extract content from URLs. Callers gate on :meth:`supports_extract`. - - Returns a list of ``{"url", "title", "content", "raw_content", - "metadata"?, "error"?}`` dicts (``error`` only on per-URL failure). - May be ``async def``. ``kwargs`` may carry forward-compat fields - (``format``, ``include_raw``, ``max_chars``) — ignore unknown keys. - """ + """Extract content from URLs (callers gate on :meth:`supports_extract`); may be ``async def``. + Returns ``[{"url", "title", "content", "raw_content", "metadata"?, "error"?}, ...]`` (``error`` + only on per-URL failure). Ignore unknown ``kwargs`` (``format``, ``include_raw``, ``max_chars``).""" raise NotImplementedError( f"{self.name} does not support extract (override supports_extract)" )