cli-agent-runner 0.1.32__tar.gz → 0.1.34__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.gitignore +3 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CHANGELOG.md +22 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CONTRIBUTING.md +3 -8
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/PKG-INFO +3 -3
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/README.md +2 -2
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_emit.py +34 -9
- cli_agent_runner-0.1.34/agent_runner/_throttle.py +133 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/api_types.py +1 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/_constants.py +18 -2
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/__init__.py +2 -0
- cli_agent_runner-0.1.34/agent_runner/cli/events_cmd.py +188 -0
- cli_agent_runner-0.1.34/agent_runner/cli/peek_cmd.py +85 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/serve_cmd.py +5 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/metrics.py +28 -2
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/monitor.py +1 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/runner.py +35 -7
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/architecture.md +1 -1
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/commands.md +20 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/long-running-agents.md +5 -4
- cli_agent_runner-0.1.34/docs/migrations/0.1.33.md +88 -0
- cli_agent_runner-0.1.34/docs/migrations/0.1.34.md +110 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/plugins.md +125 -3
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/thesis.md +38 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/pyproject.toml +2 -1
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_architecture.py +2 -2
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_module_boundaries.py +1 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_types.py +38 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_claude_error_detector.py +16 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_events.py +61 -0
- cli_agent_runner-0.1.34/tests/unit/test_events_cmd.py +179 -0
- cli_agent_runner-0.1.34/tests/unit/test_peek_select.py +41 -0
- cli_agent_runner-0.1.34/tests/unit/test_runner_throttle.py +339 -0
- cli_agent_runner-0.1.32/.githooks/commit-msg +0 -33
- cli_agent_runner-0.1.32/agent_runner/_throttle.py +0 -63
- cli_agent_runner-0.1.32/agent_runner/cli/peek_cmd.py +0 -156
- cli_agent_runner-0.1.32/tests/unit/test_peek_select.py +0 -76
- cli_agent_runner-0.1.32/tests/unit/test_runner_throttle.py +0 -125
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/LICENSE +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/README.zh.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/build.sh +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/README.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/configuration.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/events.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/docs/runbook.md +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.34] - 2026-05-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- New verb `agent-runner events --kind K[,K2,...] [--window N] [--tail]` for event-stream observation. One-shot or streaming mode (JSON Lines output). Verbs: 13 → 14.
|
|
14
|
+
- `SystemMetrics.agent_process_count: int` — `pgrep -xc` of agent binary basename, host-wide. Surfaces orphan agent processes in peek output.
|
|
15
|
+
- `docs/plugins.md` worked example for custom monitor detector with plugin-emitted exempt flag — pattern for project-specific detectors that exclude exempt-by-design rounds.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
- `peek --select events.<kind>` selector (mis-placed in state-snapshot verb). Use `events --kind <kind>` instead. 1-line `s///` migration.
|
|
19
|
+
|
|
20
|
+
See `docs/migrations/0.1.34.md`.
|
|
21
|
+
|
|
22
|
+
## [0.1.33] - 2026-05-19
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `_5XX_STATUSES` includes 529 (Anthropic's "overloaded") — now classified as `api_transient_5xx`.
|
|
26
|
+
- Exp backoff for estimated-class transient errors (`rate_limit_model` / `api_transient_5xx` / `api_timeout`): consecutive failures multiply the wait `2^N` capped at 32× and 30 minutes absolute. Server-authoritative `rate_limit_account` unchanged.
|
|
27
|
+
- `transient_error_backoff_capped` event gains `original_reset_at_epoch`, `applied_reset_at_epoch`, `consecutive_count`, `capped_by_absolute_max` fields for backoff-curve observability.
|
|
28
|
+
- `docs/thesis.md` names the server-authoritative vs estimated reset principle.
|
|
29
|
+
|
|
30
|
+
See `docs/migrations/0.1.33.md`.
|
|
31
|
+
|
|
10
32
|
## [0.1.32] - 2026-05-18
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -9,7 +9,6 @@ git clone https://github.com/wan9yu/cli-agent-runner.git
|
|
|
9
9
|
cd cli-agent-runner
|
|
10
10
|
python3 -m venv .venv && source .venv/bin/activate
|
|
11
11
|
pip install -e ".[dev]"
|
|
12
|
-
git config core.hooksPath .githooks # enables the commit-msg lint hook
|
|
13
12
|
./build.sh check
|
|
14
13
|
```
|
|
15
14
|
|
|
@@ -17,13 +16,6 @@ git config core.hooksPath .githooks # enables the commit-msg lint hook
|
|
|
17
16
|
+ integration tests, the literate quickstart, and the docs CI gate. It's
|
|
18
17
|
what GitHub Actions runs on every push and PR.
|
|
19
18
|
|
|
20
|
-
`git config core.hooksPath .githooks` activates the in-repo
|
|
21
|
-
[`.githooks/commit-msg`](.githooks/commit-msg) hook which rejects commit
|
|
22
|
-
messages containing `Co-Authored-By:` trailers, robot emojis, or other
|
|
23
|
-
AI-tool attribution patterns. The same check runs in CI (`lint-commits`
|
|
24
|
-
job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
|
|
25
|
-
— defense in depth.
|
|
26
|
-
|
|
27
19
|
## Workflow
|
|
28
20
|
|
|
29
21
|
1. Open an issue first for non-trivial changes — saves wasted work on both sides.
|
|
@@ -33,6 +25,9 @@ job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
|
|
|
33
25
|
5. Run `./build.sh check` locally before pushing.
|
|
34
26
|
6. Conventional Commits: `feat:` / `fix:` / `docs:` / `refactor:` / `test:` /
|
|
35
27
|
`chore:` / `ci:` / `build:` / `perf:`. Subjects in English, imperative mood.
|
|
28
|
+
CI (`lint-commits` job) and `tests/invariants/test_no_ai_signatures.py`
|
|
29
|
+
reject auto-generated trailers and robot signatures — keep messages
|
|
30
|
+
human-authored.
|
|
36
31
|
|
|
37
32
|
## Architecture / docs
|
|
38
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.34
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -80,14 +80,14 @@ agent-runner monitor # live anomaly detection
|
|
|
80
80
|
|
|
81
81
|
Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
82
82
|
|
|
83
|
-
##
|
|
83
|
+
## 14 verbs
|
|
84
84
|
|
|
85
85
|
| Lifecycle | Observation |
|
|
86
86
|
|---|---|
|
|
87
87
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
88
88
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
89
89
|
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
90
|
-
| `round` / `serve` | |
|
|
90
|
+
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
91
91
|
|
|
92
92
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
93
93
|
|
|
@@ -43,14 +43,14 @@ agent-runner monitor # live anomaly detection
|
|
|
43
43
|
|
|
44
44
|
Full walkthrough: [`docs/quickstart.md`](docs/quickstart.md).
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 14 verbs
|
|
47
47
|
|
|
48
48
|
| Lifecycle | Observation |
|
|
49
49
|
|---|---|
|
|
50
50
|
| `init` / `install` / `uninstall` | `peek` — state snapshot |
|
|
51
51
|
| `start` / `stop` / `kill` / `cancel` | `watch` — peek in a refresh loop |
|
|
52
52
|
| `restart` / `status` | `monitor` — 11 detectors, alerts, auto-stop |
|
|
53
|
-
| `round` / `serve` | |
|
|
53
|
+
| `round` / `serve` / `upgrade` | `events` — query / stream events.jsonl |
|
|
54
54
|
|
|
55
55
|
Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
56
56
|
|
|
@@ -281,15 +281,40 @@ def emit_transient_error_backoff_capped(
|
|
|
281
281
|
agent: str,
|
|
282
282
|
requested_sleep_s: int,
|
|
283
283
|
applied_sleep_s: int,
|
|
284
|
+
original_reset_at_epoch: int | None = None,
|
|
285
|
+
applied_reset_at_epoch: int | None = None,
|
|
286
|
+
consecutive_count: int | None = None,
|
|
287
|
+
capped_by_absolute_max: bool | None = None,
|
|
284
288
|
) -> None:
|
|
285
|
-
"""Emit
|
|
289
|
+
"""Emit when supervisor adjusts the plugin-emitted transient back-off.
|
|
290
|
+
|
|
291
|
+
Fires in two cases:
|
|
292
|
+
1. **Exp backoff applied** (0.1.33+): estimated-class transient errors
|
|
293
|
+
(`rate_limit_model` / `api_transient_5xx` / `api_timeout`) doubled
|
|
294
|
+
on consecutive failures. ``consecutive_count`` > 1, multiplier > 1×.
|
|
295
|
+
2. **Defensive cap hit** (0.1.20+): malformed `reset_at_epoch` or the
|
|
296
|
+
30-min absolute cap clipped the wait. ``capped_by_absolute_max`` True.
|
|
297
|
+
|
|
298
|
+
Fields ``original_reset_at_epoch`` / ``applied_reset_at_epoch`` /
|
|
299
|
+
``consecutive_count`` / ``capped_by_absolute_max`` are 0.1.33+. Older
|
|
300
|
+
callers that pass only the first 4 kwargs continue to work; the new
|
|
301
|
+
fields are omitted from the payload when None.
|
|
302
|
+
"""
|
|
286
303
|
from agent_runner.events import TRANSIENT_ERROR_BACKOFF_CAPPED, emit
|
|
287
304
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
kwargs: dict = {
|
|
306
|
+
"classification": classification,
|
|
307
|
+
"agent": agent,
|
|
308
|
+
"requested_sleep_s": requested_sleep_s,
|
|
309
|
+
"applied_sleep_s": applied_sleep_s,
|
|
310
|
+
}
|
|
311
|
+
if original_reset_at_epoch is not None:
|
|
312
|
+
kwargs["original_reset_at_epoch"] = original_reset_at_epoch
|
|
313
|
+
if applied_reset_at_epoch is not None:
|
|
314
|
+
kwargs["applied_reset_at_epoch"] = applied_reset_at_epoch
|
|
315
|
+
if consecutive_count is not None:
|
|
316
|
+
kwargs["consecutive_count"] = consecutive_count
|
|
317
|
+
if capped_by_absolute_max is not None:
|
|
318
|
+
kwargs["capped_by_absolute_max"] = capped_by_absolute_max
|
|
319
|
+
|
|
320
|
+
emit(log_dir, TRANSIENT_ERROR_BACKOFF_CAPPED, **kwargs)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Throttle state helpers — read events.jsonl tail for transient error state.
|
|
2
|
+
|
|
3
|
+
Internal module. Callers: runner.py (serve loop back-off), api.py (peek).
|
|
4
|
+
Separated from runner.py to satisfy the ouroboros defense: runner.py writes
|
|
5
|
+
events.jsonl but must never read it back (§3 module boundary invariant).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from collections import deque
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from agent_runner.api_types import TransientErrorState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_throttle_state(log_dir: Path) -> TransientErrorState | None:
|
|
20
|
+
"""Scan events.jsonl tail for latest unmatched transient error.
|
|
21
|
+
|
|
22
|
+
Reads `transient_error_detected` / `transient_error_recovered` event names.
|
|
23
|
+
Returns TransientErrorState if currently throttled (reset still in future,
|
|
24
|
+
no matching recovered after). Restart-safe.
|
|
25
|
+
"""
|
|
26
|
+
candidates = sorted(log_dir.glob("events-*.jsonl"))
|
|
27
|
+
if not candidates:
|
|
28
|
+
return None
|
|
29
|
+
with candidates[-1].open() as f:
|
|
30
|
+
tail = deque(f, maxlen=100)
|
|
31
|
+
events: list[dict[str, Any]] = []
|
|
32
|
+
for line in tail:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
try:
|
|
37
|
+
events.append(json.loads(line))
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
latest_detected: dict[str, Any] | None = None
|
|
42
|
+
for ev in reversed(events):
|
|
43
|
+
kind = ev.get("event")
|
|
44
|
+
if kind == "transient_error_recovered":
|
|
45
|
+
return None
|
|
46
|
+
if kind == "transient_error_detected":
|
|
47
|
+
latest_detected = ev
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if latest_detected is None:
|
|
51
|
+
return None
|
|
52
|
+
reset_at = int(latest_detected.get("reset_at_epoch", 0))
|
|
53
|
+
if reset_at <= time.time():
|
|
54
|
+
return None # Reset already passed without recovery emit; treat as recovered
|
|
55
|
+
|
|
56
|
+
classification = str(latest_detected.get("classification", "rate_limit_account"))
|
|
57
|
+
|
|
58
|
+
return TransientErrorState(
|
|
59
|
+
reset_at_epoch=reset_at,
|
|
60
|
+
classification=classification,
|
|
61
|
+
agent=str(latest_detected.get("agent", "unknown")),
|
|
62
|
+
since_round=int(latest_detected.get("round_num", 0)),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Module-level supervisor state — bucket → consecutive-failure count.
|
|
67
|
+
# Cleared by reset_counters() or by serve restart.
|
|
68
|
+
_consecutive_failures: dict[str, int] = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compute_adjusted_reset_at(
|
|
72
|
+
*,
|
|
73
|
+
classification: str,
|
|
74
|
+
original_reset_at_epoch: int,
|
|
75
|
+
agent: str,
|
|
76
|
+
log_dir: Path,
|
|
77
|
+
) -> tuple[int, int, bool]:
|
|
78
|
+
"""Apply exp backoff for estimated-class transient errors.
|
|
79
|
+
|
|
80
|
+
Returns (applied_reset_at_epoch, consecutive_count, capped_by_absolute_max).
|
|
81
|
+
|
|
82
|
+
For server-authoritative classification (``rate_limit_account``): returns
|
|
83
|
+
the original reset epoch verbatim, never increments the counter, and
|
|
84
|
+
never emits an adjustment event. Anthropic's resetsAt is authoritative.
|
|
85
|
+
|
|
86
|
+
For estimated classifications (``rate_limit_model``, ``api_transient_5xx``,
|
|
87
|
+
``api_timeout``): increments the counter for this bucket, computes
|
|
88
|
+
duration = base × 2^min(n, _EXP_CAP), caps at _ABSOLUTE_CAP_S, emits
|
|
89
|
+
``transient_error_backoff_capped`` if multiplier > 1 or capped.
|
|
90
|
+
"""
|
|
91
|
+
from agent_runner._emit import emit_transient_error_backoff_capped
|
|
92
|
+
from agent_runner.builtin_plugins._constants import (
|
|
93
|
+
_ABSOLUTE_CAP_S,
|
|
94
|
+
_BACK_OFF_DEFAULTS,
|
|
95
|
+
_EXP_CAP,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if classification == "rate_limit_account":
|
|
99
|
+
# Server-authoritative: respect resetsAt verbatim, no counter touch.
|
|
100
|
+
return (original_reset_at_epoch, 0, False)
|
|
101
|
+
|
|
102
|
+
# Estimated class: apply exp backoff.
|
|
103
|
+
base = _BACK_OFF_DEFAULTS[classification]
|
|
104
|
+
n = _consecutive_failures.get(classification, 0)
|
|
105
|
+
multiplier = 2 ** min(n, _EXP_CAP)
|
|
106
|
+
extended_duration = base * multiplier
|
|
107
|
+
capped_by_absolute_max = extended_duration > _ABSOLUTE_CAP_S
|
|
108
|
+
applied_duration = min(extended_duration, _ABSOLUTE_CAP_S)
|
|
109
|
+
applied_reset_at = int(time.time()) + applied_duration
|
|
110
|
+
|
|
111
|
+
new_count = n + 1
|
|
112
|
+
_consecutive_failures[classification] = new_count
|
|
113
|
+
|
|
114
|
+
# Emit observability event when supervisor adjusted the wait.
|
|
115
|
+
if multiplier > 1 or capped_by_absolute_max:
|
|
116
|
+
emit_transient_error_backoff_capped(
|
|
117
|
+
log_dir,
|
|
118
|
+
classification=classification,
|
|
119
|
+
agent=agent,
|
|
120
|
+
requested_sleep_s=int(base),
|
|
121
|
+
applied_sleep_s=applied_duration,
|
|
122
|
+
original_reset_at_epoch=original_reset_at_epoch,
|
|
123
|
+
applied_reset_at_epoch=applied_reset_at,
|
|
124
|
+
consecutive_count=new_count,
|
|
125
|
+
capped_by_absolute_max=capped_by_absolute_max,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return (applied_reset_at, new_count, capped_by_absolute_max)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def reset_counters() -> None:
|
|
132
|
+
"""Clear all bucket counters. Called by serve loop when no active throttle."""
|
|
133
|
+
_consecutive_failures.clear()
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.34'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 34)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{cli_agent_runner-0.1.32 → cli_agent_runner-0.1.34}/agent_runner/builtin_plugins/_constants.py
RENAMED
|
@@ -21,9 +21,11 @@ _BACK_OFF_DEFAULTS: dict[str, int] = {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
# 5xx codes treated as transient (retry-worthy server errors per RFC 9110):
|
|
24
|
-
# 500=unexpected, 502=bad gateway, 503=unavailable, 504=gateway timeout
|
|
24
|
+
# 500=unexpected, 502=bad gateway, 503=unavailable, 504=gateway timeout,
|
|
25
|
+
# 529=overloaded (Anthropic's non-RFC code emitted during sustained capacity
|
|
26
|
+
# issues; treated as transient per Anthropic SDK behavior).
|
|
25
27
|
# Excluded: 501 (not implemented = permanent), 505 (HTTP version mismatch).
|
|
26
|
-
_5XX_STATUSES: frozenset[int] = frozenset({500, 502, 503, 504})
|
|
28
|
+
_5XX_STATUSES: frozenset[int] = frozenset({500, 502, 503, 504, 529})
|
|
27
29
|
|
|
28
30
|
_CLASSIFICATIONS: frozenset[str] = frozenset(
|
|
29
31
|
{
|
|
@@ -38,3 +40,17 @@ _CLASSIFICATIONS: frozenset[str] = frozenset(
|
|
|
38
40
|
rate_limit_account uses server-provided resetsAt (excluded from
|
|
39
41
|
_BACK_OFF_DEFAULTS table); others use defaults from that table.
|
|
40
42
|
"""
|
|
43
|
+
|
|
44
|
+
_EXP_CAP: int = 5
|
|
45
|
+
"""Maximum exponent for transient-error consecutive backoff: 2^5 = 32×.
|
|
46
|
+
|
|
47
|
+
Beyond this, the multiplier plateaus. Combined with _ABSOLUTE_CAP_S, this
|
|
48
|
+
prevents runaway wait times during sustained outages (max wait = 30min).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_ABSOLUTE_CAP_S: int = 1800
|
|
52
|
+
"""Absolute upper bound on supervisor-applied transient back-off (30 min).
|
|
53
|
+
|
|
54
|
+
Applies after exp multiplier — even if base × 2^5 exceeds this, the wait
|
|
55
|
+
is clipped here. Defends against an indefinitely-stuck supervisor.
|
|
56
|
+
"""
|
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
|
|
18
18
|
from agent_runner import __version__
|
|
19
19
|
from agent_runner.cli import (
|
|
20
|
+
events_cmd,
|
|
20
21
|
init_cmd,
|
|
21
22
|
install_cmd,
|
|
22
23
|
monitor_cmd,
|
|
@@ -63,6 +64,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
63
64
|
install_cmd.add_parser(sub, parent)
|
|
64
65
|
service_cmd.add_parser(sub, parent)
|
|
65
66
|
peek_cmd.add_parser(sub, parent)
|
|
67
|
+
events_cmd.add_parser(sub, parent)
|
|
66
68
|
monitor_cmd.add_parser(sub, parent)
|
|
67
69
|
serve_cmd.add_parser(sub, parent)
|
|
68
70
|
round_cmd.add_parser(sub, parent)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""agent-runner events — event-stream observation verb (0.1.34+).
|
|
2
|
+
|
|
3
|
+
One-shot (--window N) or streaming (--tail) query against events.jsonl.
|
|
4
|
+
JSON Lines output (one JSON object per line, no pretty-print).
|
|
5
|
+
|
|
6
|
+
Current-month scope only. Tail mode follows month rollover via per-poll glob.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import signal
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# Sentinel for "user did not explicitly set --window" so we can detect
|
|
20
|
+
# --window + --tail combinations. argparse mutually-exclusive group would
|
|
21
|
+
# be cleaner but argparse doesn't support "exclusive only when X has value Y".
|
|
22
|
+
_WINDOW_DEFAULT_SENTINEL = -1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _positive_int(s: str) -> int:
|
|
26
|
+
"""Parse positive integer (duplicate of peek_cmd._positive_int; KISS)."""
|
|
27
|
+
try:
|
|
28
|
+
n = int(s)
|
|
29
|
+
except ValueError as e:
|
|
30
|
+
raise argparse.ArgumentTypeError(f"expects positive int, got {s!r}") from e
|
|
31
|
+
if n <= 0:
|
|
32
|
+
raise argparse.ArgumentTypeError(f"expects positive int (> 0), got {n}")
|
|
33
|
+
return n
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_kinds(raw: str) -> set[str]:
|
|
37
|
+
"""Parse comma-separated kinds; strip whitespace; reject empty."""
|
|
38
|
+
parts = [k.strip() for k in (raw or "").split(",") if k.strip()]
|
|
39
|
+
return set(parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def add_parser(sub, parent) -> None:
|
|
43
|
+
p = sub.add_parser(
|
|
44
|
+
"events",
|
|
45
|
+
parents=[parent],
|
|
46
|
+
help="Query / stream events from events.jsonl by kind",
|
|
47
|
+
)
|
|
48
|
+
p.add_argument(
|
|
49
|
+
"--kind",
|
|
50
|
+
type=str,
|
|
51
|
+
required=True,
|
|
52
|
+
metavar="K[,K2,...]",
|
|
53
|
+
help="Comma-separated event kinds (OR-filtered). At least one required.",
|
|
54
|
+
)
|
|
55
|
+
p.add_argument(
|
|
56
|
+
"--window",
|
|
57
|
+
type=_positive_int,
|
|
58
|
+
default=_WINDOW_DEFAULT_SENTINEL,
|
|
59
|
+
metavar="N",
|
|
60
|
+
help="One-shot mode: emit last N matching events (default 10).",
|
|
61
|
+
)
|
|
62
|
+
p.add_argument(
|
|
63
|
+
"--tail",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help=("Streaming mode: emit each new matching event as it fires (blocks until SIGINT)."),
|
|
66
|
+
)
|
|
67
|
+
p.set_defaults(func=cmd_events)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_log_dir(args) -> Path:
|
|
71
|
+
"""Resolve log_dir from --config (used by both cmd_events and tests)."""
|
|
72
|
+
if getattr(args, "_log_dir_override", None) is not None:
|
|
73
|
+
return args._log_dir_override
|
|
74
|
+
from agent_runner.cli.common import work_dir_from_args
|
|
75
|
+
from agent_runner.config import load_config
|
|
76
|
+
|
|
77
|
+
cfg = load_config(work_dir_from_args(args) / "agent-runner.toml")
|
|
78
|
+
return cfg.runtime.log_dir
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_events(args) -> int:
|
|
82
|
+
kind_set = _parse_kinds(args.kind)
|
|
83
|
+
if not kind_set:
|
|
84
|
+
print(
|
|
85
|
+
"Error: --kind requires at least one non-empty event kind",
|
|
86
|
+
file=sys.stderr,
|
|
87
|
+
)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
window_explicit = getattr(args, "_window_explicit", False) or (
|
|
91
|
+
args.window != _WINDOW_DEFAULT_SENTINEL
|
|
92
|
+
)
|
|
93
|
+
if args.tail and window_explicit:
|
|
94
|
+
print(
|
|
95
|
+
"Error: --window and --tail are mutually exclusive",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
return 2
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
log_dir = _resolve_log_dir(args)
|
|
102
|
+
except FileNotFoundError as e:
|
|
103
|
+
print(f"Error: config not found: {e}", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
if args.tail:
|
|
107
|
+
return _tail_events(log_dir, kind_set)
|
|
108
|
+
|
|
109
|
+
window = args.window if args.window != _WINDOW_DEFAULT_SENTINEL else 10
|
|
110
|
+
return _query_events(log_dir, kind_set, window)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _current_month_events_file(log_dir: Path) -> Path:
|
|
114
|
+
month = datetime.now(UTC).strftime("%Y-%m")
|
|
115
|
+
return log_dir / f"events-{month}.jsonl"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _query_events(log_dir: Path, kind_set: set[str], window: int) -> int:
|
|
119
|
+
"""One-shot: read current-month events.jsonl, filter, print last N."""
|
|
120
|
+
events_file = _current_month_events_file(log_dir)
|
|
121
|
+
if not events_file.exists():
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
matches: list[str] = []
|
|
125
|
+
try:
|
|
126
|
+
with events_file.open("r", encoding="utf-8") as f:
|
|
127
|
+
for line in f:
|
|
128
|
+
line = line.strip()
|
|
129
|
+
if not line:
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
evt = json.loads(line)
|
|
133
|
+
except json.JSONDecodeError:
|
|
134
|
+
continue
|
|
135
|
+
if evt.get("event") in kind_set:
|
|
136
|
+
matches.append(line)
|
|
137
|
+
except OSError as e:
|
|
138
|
+
print(f"Error: events file unreadable: {e}", file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
for line in matches[-window:]:
|
|
142
|
+
print(line)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _tail_events(log_dir: Path, kind_set: set[str]) -> int:
|
|
147
|
+
"""Streaming: poll current-month events.jsonl at 1s interval; emit each
|
|
148
|
+
new matching line as it fires. Blocks until SIGINT (KeyboardInterrupt).
|
|
149
|
+
Follows month rollover via per-poll glob.
|
|
150
|
+
"""
|
|
151
|
+
last_size = 0
|
|
152
|
+
current_file: Path | None = None
|
|
153
|
+
|
|
154
|
+
def _handle_sigint(_signum, _frame):
|
|
155
|
+
raise KeyboardInterrupt()
|
|
156
|
+
|
|
157
|
+
signal.signal(signal.SIGINT, _handle_sigint)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
events_file = _current_month_events_file(log_dir)
|
|
162
|
+
if events_file != current_file:
|
|
163
|
+
# Month rollover OR first iteration: reset offset
|
|
164
|
+
current_file = events_file
|
|
165
|
+
last_size = events_file.stat().st_size if events_file.exists() else 0
|
|
166
|
+
|
|
167
|
+
if events_file.exists():
|
|
168
|
+
size = events_file.stat().st_size
|
|
169
|
+
if size > last_size:
|
|
170
|
+
with events_file.open("r", encoding="utf-8") as f:
|
|
171
|
+
f.seek(last_size)
|
|
172
|
+
for line in f:
|
|
173
|
+
line = line.strip()
|
|
174
|
+
if not line:
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
evt = json.loads(line)
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
continue
|
|
180
|
+
if evt.get("event") in kind_set:
|
|
181
|
+
print(line, flush=True)
|
|
182
|
+
last_size = size
|
|
183
|
+
elif size < last_size:
|
|
184
|
+
# File truncated / rotated underneath us; reset
|
|
185
|
+
last_size = 0
|
|
186
|
+
time.sleep(1.0)
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
return 0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""peek and watch subcommands — snapshot + auto-refresh."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from agent_runner import api
|
|
10
|
+
from agent_runner.cli.common import emit, fail, work_dir_from_args
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _round_arg(s: str) -> int | str:
|
|
14
|
+
if s == "latest":
|
|
15
|
+
return s
|
|
16
|
+
try:
|
|
17
|
+
return int(s)
|
|
18
|
+
except ValueError as e:
|
|
19
|
+
raise argparse.ArgumentTypeError(f"--round expects int or 'latest', got {s!r}") from e
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_parser(sub, parent) -> None:
|
|
23
|
+
for verb, fn in (("peek", cmd_peek), ("watch", cmd_watch)):
|
|
24
|
+
p = sub.add_parser(
|
|
25
|
+
verb, parents=[parent], help=f"{verb} project state with optional drill-down"
|
|
26
|
+
)
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"--round",
|
|
29
|
+
type=_round_arg,
|
|
30
|
+
default=None,
|
|
31
|
+
metavar="N",
|
|
32
|
+
help="Drill into round N (int or 'latest')",
|
|
33
|
+
)
|
|
34
|
+
p.add_argument("--log", action="store_true", help="Include current round's log tail")
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"--events", type=int, default=None, metavar="N", help="Include last N events"
|
|
37
|
+
)
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"--select",
|
|
40
|
+
type=str,
|
|
41
|
+
default=None,
|
|
42
|
+
help=(
|
|
43
|
+
"Selector: dot-path (e.g. system.disk_used_pct) extracts a subtree from "
|
|
44
|
+
"peek state. For event-stream queries, use the dedicated `events` verb."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
if verb == "watch":
|
|
48
|
+
p.add_argument(
|
|
49
|
+
"--interval",
|
|
50
|
+
type=int,
|
|
51
|
+
default=2,
|
|
52
|
+
metavar="SECONDS",
|
|
53
|
+
help="Refresh interval (default 2)",
|
|
54
|
+
)
|
|
55
|
+
p.set_defaults(func=fn)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_peek(args) -> int:
|
|
59
|
+
select = args.select
|
|
60
|
+
try:
|
|
61
|
+
result = api.peek(
|
|
62
|
+
work_dir_from_args(args),
|
|
63
|
+
round=args.round,
|
|
64
|
+
log=args.log,
|
|
65
|
+
events=args.events,
|
|
66
|
+
select=select,
|
|
67
|
+
)
|
|
68
|
+
except KeyError as e:
|
|
69
|
+
return fail(str(e))
|
|
70
|
+
except FileNotFoundError as e:
|
|
71
|
+
return fail(f"config not found: {e}")
|
|
72
|
+
emit(result, json_mode=getattr(args, "json", False))
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_watch(args) -> int:
|
|
77
|
+
while True:
|
|
78
|
+
sys.stdout.write("\x1b[2J\x1b[H")
|
|
79
|
+
rc = cmd_peek(args)
|
|
80
|
+
if rc != 0:
|
|
81
|
+
return rc
|
|
82
|
+
try:
|
|
83
|
+
time.sleep(args.interval)
|
|
84
|
+
except KeyboardInterrupt:
|
|
85
|
+
return 0
|
|
@@ -20,6 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
from agent_runner._substrate import compute_git_head, compute_paths_hash
|
|
22
22
|
from agent_runner._throttle import _check_throttle_state
|
|
23
|
+
from agent_runner._throttle import reset_counters as _reset_counters
|
|
23
24
|
from agent_runner.api import (
|
|
24
25
|
check_self_terminated_sentinel,
|
|
25
26
|
emit_fresh_eyes_round_triggered,
|
|
@@ -151,6 +152,10 @@ def cmd(args) -> int:
|
|
|
151
152
|
elif action == "stop":
|
|
152
153
|
emit_rate_limit_stop(log_dir)
|
|
153
154
|
break
|
|
155
|
+
else:
|
|
156
|
+
# No active throttle this round — supervisor counters can reset.
|
|
157
|
+
# Next failure (if any) restarts the exp backoff curve from 1×.
|
|
158
|
+
_reset_counters()
|
|
154
159
|
if stop_file is not None and stop_file.exists():
|
|
155
160
|
try:
|
|
156
161
|
content = stop_file.read_text(encoding="utf-8", errors="replace")[:200]
|