Follow-up to #120339 from its independent re-review. - Single-flight: the restore runs under an OS lock on <git dir>/hermes-update-pull.claim (flock on POSIX, msvcrt.locking on Windows; owner pid written for humans). The kernel drops the lock with its owner, so a dead launch's claim is broken without a check-then-unlink race. A launch that loses waits up to 10 s, then re-checks: if the marker is gone the tree changed under it and it returns True (relaunch) instead of importing a half-restored tree; if the holder is still busy it prints a neutral note and never touches the tree. - index.lock is removed only by the claim holder and only when no live process has it open (/proc on Linux; Windows refuses the unlink of an open file; elsewhere the claim is the guard). - The failure message no longer prints `git reset --hard <pre>`: that wipes the uncommitted edits the restore keeps. It names git's error and says the next launch retries. - HEAD already moved is checked before merge/rebase state, so a git killed after committing its merge but before deleting MERGE_HEAD spends the marker instead of pinning it forever. - MERGE_HEAD == the marker's target (the updater's own merge, killed mid-conflict): print `git -C <root> merge --abort`, then relaunch, once per launch (plus the stash name if any). - `hermes-agent` (agent.legacy_cli:main) now repairs at the top of agent/legacy_cli.py, before argparse and run_agent; the entry-point spy test covers it. - Directories git created for added files are removed bottom-up when empty; a dir pre has, or one holding an untracked file, is kept.
95 lines
4.6 KiB
Python
95 lines
4.6 KiB
Python
"""Argument layer for the legacy ``hermes-agent`` runner (``run_agent.main``).
|
|
|
|
A console script calls its target with no arguments, so pointing ``hermes-agent``
|
|
at ``run_agent.main`` ignored argv entirely: ``--help``, ``--version`` and a bare
|
|
invocation all ran a real model turn with ``main()``'s built-in demo query, and
|
|
``--query`` was silently dropped (#54648). ``python run_agent.py`` routes through
|
|
here too, so the installer's PATH launcher behaves the same way.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# hermes_bootstrap first (UTF-8 stdio on Windows; no-op on POSIX), like every other entry point.
|
|
try:
|
|
import hermes_bootstrap # noqa: F401
|
|
except ModuleNotFoundError:
|
|
pass # partial `hermes update` — only skips the Windows UTF-8 stdio setup
|
|
|
|
# The `hermes-agent` console script lands here without hermes_cli.main: repair a `hermes update` killed
|
|
# while git wrote the new tree before importing anything else from the checkout.
|
|
from hermes_cli import _early_recovery
|
|
|
|
if _early_recovery.restore_interrupted_pull():
|
|
_early_recovery.relaunch_after_restore()
|
|
|
|
import argparse # noqa: E402
|
|
from typing import Callable, List, Optional # noqa: E402
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
from hermes_cli import __release_date__, __version__
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog="hermes-agent",
|
|
description="Legacy single-query Hermes Agent runner. For the full CLI use `hermes`.",
|
|
)
|
|
parser.add_argument("--version", action="version", version=f"Hermes Agent v{__version__} ({__release_date__})")
|
|
parser.add_argument("prompt", nargs="*", help="query to run (same as --query)")
|
|
parser.add_argument("--query", "-q", help="natural-language query to run")
|
|
parser.add_argument("--model", default="", help="model id (provider/model)")
|
|
parser.add_argument("--api-key", "--api_key", dest="api_key", help="API key for the model endpoint")
|
|
parser.add_argument("--base-url", "--base_url", dest="base_url", default="", help="model API base URL")
|
|
parser.add_argument("--max-turns", "--max_turns", dest="max_turns", type=int, default=10,
|
|
help="maximum API call iterations (default: 10)")
|
|
parser.add_argument("--enabled-toolsets", "--enabled_toolsets", dest="enabled_toolsets",
|
|
help="comma-separated toolsets to enable")
|
|
parser.add_argument("--disabled-toolsets", "--disabled_toolsets", dest="disabled_toolsets",
|
|
help="comma-separated toolsets to disable")
|
|
parser.add_argument("--list-tools", "--list_tools", dest="list_tools", action="store_true",
|
|
help="list available tools and exit")
|
|
parser.add_argument("--save-trajectories", "--save_trajectories", dest="save_trajectories",
|
|
action="store_true", help="append the conversation to trajectory JSONL files")
|
|
parser.add_argument("--save-sample", "--save_sample", dest="save_sample", action="store_true",
|
|
help="save one trajectory sample to a UUID-named file")
|
|
parser.add_argument("--verbose", action="store_true", help="verbose logging")
|
|
parser.add_argument("--log-prefix-chars", "--log_prefix_chars", dest="log_prefix_chars", type=int,
|
|
default=20, help="characters shown in tool-call log previews (default: 20)")
|
|
return parser
|
|
|
|
|
|
def main(argv: Optional[List[str]] = None, *, run: Optional[Callable[..., object]] = None) -> int:
|
|
"""Parse ``argv`` (default ``sys.argv[1:]``) and run one query through ``run_agent.main``.
|
|
|
|
Metadata flags and a bare invocation never reach the runner; ``run`` lets
|
|
``python run_agent.py`` pass its own ``main`` instead of importing the module twice.
|
|
"""
|
|
parser = _build_parser()
|
|
args = parser.parse_args(argv)
|
|
positional = " ".join(args.prompt).strip()
|
|
if args.query and positional:
|
|
parser.error("pass the query either positionally or via --query, not both")
|
|
query = args.query or positional or None
|
|
if query is None and not args.list_tools:
|
|
parser.print_help()
|
|
print("\nNo query given: pass one with --query (or run `hermes` for the interactive CLI).")
|
|
return 0
|
|
|
|
if run is None:
|
|
from run_agent import main as run
|
|
|
|
run(
|
|
query=query,
|
|
model=args.model,
|
|
api_key=args.api_key,
|
|
base_url=args.base_url,
|
|
max_turns=args.max_turns,
|
|
enabled_toolsets=args.enabled_toolsets,
|
|
disabled_toolsets=args.disabled_toolsets,
|
|
list_tools=args.list_tools,
|
|
save_trajectories=args.save_trajectories,
|
|
save_sample=args.save_sample,
|
|
verbose=args.verbose,
|
|
log_prefix_chars=args.log_prefix_chars,
|
|
)
|
|
return 0
|