claude-code-generator 0.4.10__tar.gz → 0.4.12__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.
- {claude_code_generator-0.4.10/src/claude_code_generator.egg-info → claude_code_generator-0.4.12}/PKG-INFO +1 -1
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/pyproject.toml +1 -1
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/__init__.py +1 -1
- claude_code_generator-0.4.12/src/code_generator/orchestrator/ollama_budget.py +215 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase1_plan.py +95 -21
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_ollama_budget.py +86 -60
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase1.py +97 -0
- claude_code_generator-0.4.10/src/code_generator/orchestrator/ollama_budget.py +0 -116
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/LICENSE +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/README.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/setup.cfg +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/requires.txt +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/top_level.txt +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/agents.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/cli.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_bench_io.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_crash_recovery.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_detect.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_dispatch.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_resume.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_validators.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench_compare.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench_export.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/generate.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/init.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/optimize.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/review.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/status.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/effort.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/env.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/core.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/issues.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/labels.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/milestones.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/git_ops.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/logging_setup.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/memory.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_client_lifecycle.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_comments.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_memory_writers.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/cycle_loop.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase2_review.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase5_closure.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase6_test.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase7_commit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/preflight.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/hashes.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-review.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/repo_info.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/repomap.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/requirements_structure.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/_telemetry.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/batch.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/fake_runner.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/mcp.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/message_parsing.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/options.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/protocol.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/rate_limit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/retry.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/sdk_runner.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/soft_reset.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/state_guard.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/subprocess_runner.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/types.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/utils.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/state.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/state_retention.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/__init__.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/angular.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/base.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/fastapi.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/finance.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/fullstack.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/nestjs.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/python-cli.md +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_agents.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_compare.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_export.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_fixture.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_regression.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_changelog.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_claude_md.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_client_lifecycle.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_comments.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_commit_message.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_crash_recovery.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_loop.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_loop_multicycle.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_ollama_model.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_prompts.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_delta_planning.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_dependencies.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_detect.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_no_default_max_turns.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_ollama_model_guide.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_ollama_pro.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_effective_model_routing.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_effort.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_env.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate_ollama.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate_resume.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_labels.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_milestones.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_repo_threading.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_submodules.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_git_ops.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_init.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_logging_setup.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_max_turns_cli_flag.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_mcp.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_memory.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_memory_writers.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_message_parsing.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_no_max_turns_in_call_sites.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_no_max_turns_literal.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_non_goals_grep_guard.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_ollama_rate_limit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_optimize.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_options.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase0.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase2.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase2_batch.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase3_4.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase5.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase6.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase7.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase_mcp_regression.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase_token_logging.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_preflight.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_preflight_ollama.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_drift.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_prefix_snapshots.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_prefix_stability.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompts.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_rate_limit.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_repo_info.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_repomap.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_requirements_structure.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_retry.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_review.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_protocol.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_protocol_annotations.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_types.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_utils.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_sdk_runner.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_sdk_runner_shared.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_session_mode.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state_guard.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state_retention.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_status.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_subprocess_runner.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_telemetry.py +0 -0
- {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-generator"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.12"
|
|
8
8
|
description = "Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Per-cycle safety backstop for the Ollama codepath.
|
|
2
|
+
|
|
3
|
+
The pre-0.4.11 design treated ``OLLAMA_TURN_BUDGET`` as a hard abort trigger
|
|
4
|
+
— cycles were aborted after exactly 200 turns regardless of whether the
|
|
5
|
+
model was making progress. That is the wrong layer: actual fault-detection
|
|
6
|
+
already lives in two places and triggers on real malfunctions, not on an
|
|
7
|
+
arbitrary counter:
|
|
8
|
+
|
|
9
|
+
* :class:`~code_generator.runner.retry.CircuitBreaker` — trips after ``N``
|
|
10
|
+
consecutive failures on a single phase call. Already wrapped around
|
|
11
|
+
Phase 2 (per-issue / batched review), Phase 3/4 (TDD implementation),
|
|
12
|
+
and Phase 5 (closure).
|
|
13
|
+
* :func:`~code_generator.runner.rate_limit.handle_ollama_429` —
|
|
14
|
+
wait-and-resume on 429s returned by the Ollama daemon.
|
|
15
|
+
|
|
16
|
+
This module therefore degrades to a **non-blocking adaptive** backstop:
|
|
17
|
+
|
|
18
|
+
* The turn counter now emits a single **WARNING** when the cycle crosses
|
|
19
|
+
the soft threshold (default 500). The pipeline is **never** aborted on
|
|
20
|
+
the turn count alone. Weak open models are chatty by design; letting
|
|
21
|
+
them run is the right call.
|
|
22
|
+
* The wall-clock cap remains a **hard abort**, but the default is raised
|
|
23
|
+
to 4 hours. It exists purely to catch a stuck daemon or a model trapped
|
|
24
|
+
in a pathological loop the CircuitBreaker cannot see (e.g. infinite
|
|
25
|
+
``end_turn``→``continue`` cycle producing no tool calls).
|
|
26
|
+
|
|
27
|
+
Both thresholds are env-overridable:
|
|
28
|
+
|
|
29
|
+
* ``OLLAMA_SOFT_TURN_WARN`` (int, positive; default 500)
|
|
30
|
+
* ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` (int, positive; default 14400)
|
|
31
|
+
|
|
32
|
+
Backwards-compatible shim: the old ``OLLAMA_TURN_BUDGET`` env variable is
|
|
33
|
+
still honoured and maps onto the soft-warn threshold, so operators with
|
|
34
|
+
existing scripts see no behaviour change beyond the abort becoming a
|
|
35
|
+
warning.
|
|
36
|
+
|
|
37
|
+
Nothing is persisted in ``state.json`` — the tracker is per-run and
|
|
38
|
+
discarded on abort or clean completion.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
import os
|
|
45
|
+
import time
|
|
46
|
+
from typing import TYPE_CHECKING
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from collections.abc import Callable
|
|
50
|
+
|
|
51
|
+
from code_generator.state import CycleState, State
|
|
52
|
+
|
|
53
|
+
_logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Threshold constants
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
_DEFAULT_SOFT_TURN_WARN = 500
|
|
60
|
+
_DEFAULT_WALLCLOCK_BUDGET_SECONDS = 14400 # 4 h
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_int_env(name: str, default: int) -> int:
|
|
64
|
+
"""Parse a positive int from the environment, falling back to *default*.
|
|
65
|
+
|
|
66
|
+
Invalid values (empty, non-numeric, zero, or negative) are silently
|
|
67
|
+
replaced with *default* — these are per-run tuning knobs, not safety
|
|
68
|
+
gates, so noisy error handling would surprise operators who meant well.
|
|
69
|
+
"""
|
|
70
|
+
raw = os.environ.get(name)
|
|
71
|
+
if raw is None or not raw.strip():
|
|
72
|
+
return default
|
|
73
|
+
try:
|
|
74
|
+
value = int(raw)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return default
|
|
77
|
+
return value if value > 0 else default
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_soft_turn_warn() -> int:
|
|
81
|
+
"""Honour the legacy ``OLLAMA_TURN_BUDGET`` env var for backwards compat."""
|
|
82
|
+
legacy = _read_int_env("OLLAMA_TURN_BUDGET", 0)
|
|
83
|
+
if legacy:
|
|
84
|
+
return legacy
|
|
85
|
+
return _read_int_env("OLLAMA_SOFT_TURN_WARN", _DEFAULT_SOFT_TURN_WARN)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
OLLAMA_SOFT_TURN_WARN = _resolve_soft_turn_warn()
|
|
89
|
+
"""Soft warning threshold on per-cycle ``num_turns``.
|
|
90
|
+
|
|
91
|
+
Defaults to 500. Override via ``OLLAMA_SOFT_TURN_WARN`` (new name) or the
|
|
92
|
+
legacy ``OLLAMA_TURN_BUDGET`` (preserved for backwards compatibility). The
|
|
93
|
+
value is **non-blocking**: the pipeline only logs a WARNING once per cycle
|
|
94
|
+
when the cumulative turn count first crosses this threshold. It never
|
|
95
|
+
aborts.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Kept as a module-level alias so existing importers (tests, scripts) keep
|
|
99
|
+
# working. The semantics are now "soft warning threshold", not "abort".
|
|
100
|
+
OLLAMA_TURN_BUDGET = OLLAMA_SOFT_TURN_WARN
|
|
101
|
+
"""Backwards-compatible alias for :data:`OLLAMA_SOFT_TURN_WARN`."""
|
|
102
|
+
|
|
103
|
+
OLLAMA_WALLCLOCK_BUDGET_SECONDS = _read_int_env(
|
|
104
|
+
"OLLAMA_WALLCLOCK_BUDGET_SECONDS", _DEFAULT_WALLCLOCK_BUDGET_SECONDS
|
|
105
|
+
)
|
|
106
|
+
"""Hard wall-clock abort threshold (seconds) per cycle on the Ollama codepath.
|
|
107
|
+
|
|
108
|
+
Defaults to 14400 (4 h); override via ``OLLAMA_WALLCLOCK_BUDGET_SECONDS``.
|
|
109
|
+
This is the only hard abort enforced by this module — it exists to catch a
|
|
110
|
+
stuck daemon or a pathological loop the per-phase
|
|
111
|
+
:class:`~code_generator.runner.retry.CircuitBreaker` cannot see.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Exception
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class OllamaBudgetExceeded(RuntimeError):
|
|
121
|
+
"""Raised when the Ollama per-cycle wall-clock backstop is exceeded.
|
|
122
|
+
|
|
123
|
+
Only fires on the wall-clock path. The turn counter now emits a WARNING
|
|
124
|
+
instead; real per-call failures are handled by the ``CircuitBreaker``
|
|
125
|
+
in :mod:`code_generator.runner.retry`.
|
|
126
|
+
|
|
127
|
+
Subclasses ``RuntimeError`` to match the existing safety-abort hierarchy
|
|
128
|
+
(e.g. :class:`~code_generator.runner.types.OverageAbort`).
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Tracker
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class OllamaBudgetTracker:
|
|
138
|
+
"""Adaptive per-cycle safety backstop; a no-op on the Anthropic Max path.
|
|
139
|
+
|
|
140
|
+
Emits one WARNING when the cycle crosses the soft turn threshold; aborts
|
|
141
|
+
only on the wall-clock backstop. Does **not** block the pipeline on the
|
|
142
|
+
turn count — real failures are the responsibility of
|
|
143
|
+
:class:`~code_generator.runner.retry.CircuitBreaker` and the rate-limit
|
|
144
|
+
handlers in :mod:`code_generator.runner.rate_limit`.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
provider_is_ollama: bool,
|
|
151
|
+
clock: Callable[[], float] | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Initialise a tracker; call :meth:`start` at cycle kickoff.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
provider_is_ollama: When False every :meth:`check` call is a
|
|
157
|
+
no-op so the Anthropic Max path stays byte-for-byte unchanged.
|
|
158
|
+
clock: Injectable monotonic clock for deterministic tests;
|
|
159
|
+
defaults to ``time.monotonic``.
|
|
160
|
+
"""
|
|
161
|
+
self._active = provider_is_ollama
|
|
162
|
+
self._clock = clock or time.monotonic
|
|
163
|
+
self._start_time: float | None = None
|
|
164
|
+
self._turn_warning_emitted = False
|
|
165
|
+
|
|
166
|
+
def start(self) -> None:
|
|
167
|
+
"""Record the cycle start time. Idempotent; only the first call matters."""
|
|
168
|
+
if self._active and self._start_time is None:
|
|
169
|
+
self._start_time = self._clock()
|
|
170
|
+
|
|
171
|
+
def check(self, state: State, cycle: CycleState | None) -> None:
|
|
172
|
+
"""Warn on soft-turn threshold; raise only on wall-clock overflow."""
|
|
173
|
+
if not self._active:
|
|
174
|
+
return
|
|
175
|
+
self._check_turn_soft_warn(state, cycle)
|
|
176
|
+
self._check_wallclock_budget()
|
|
177
|
+
|
|
178
|
+
def _check_turn_soft_warn(self, state: State, cycle: CycleState | None) -> None:
|
|
179
|
+
"""Emit one WARNING the first time the cycle crosses the soft threshold.
|
|
180
|
+
|
|
181
|
+
Never raises. Subsequent checks in the same cycle are no-ops because
|
|
182
|
+
the warning flag is sticky until the tracker is discarded.
|
|
183
|
+
"""
|
|
184
|
+
if self._turn_warning_emitted:
|
|
185
|
+
return
|
|
186
|
+
total = _sum_num_turns(state, cycle)
|
|
187
|
+
if total > OLLAMA_SOFT_TURN_WARN:
|
|
188
|
+
_logger.warning(
|
|
189
|
+
"Ollama cycle has consumed %d turns (soft threshold: %d). "
|
|
190
|
+
"Letting it run — real failures are caught by the per-phase "
|
|
191
|
+
"CircuitBreaker in runner/retry.py. Raise the threshold via "
|
|
192
|
+
"OLLAMA_SOFT_TURN_WARN or the legacy OLLAMA_TURN_BUDGET env "
|
|
193
|
+
"var to silence this warning.",
|
|
194
|
+
total,
|
|
195
|
+
OLLAMA_SOFT_TURN_WARN,
|
|
196
|
+
)
|
|
197
|
+
self._turn_warning_emitted = True
|
|
198
|
+
|
|
199
|
+
def _check_wallclock_budget(self) -> None:
|
|
200
|
+
if self._start_time is None:
|
|
201
|
+
return
|
|
202
|
+
elapsed = self._clock() - self._start_time
|
|
203
|
+
if elapsed > OLLAMA_WALLCLOCK_BUDGET_SECONDS:
|
|
204
|
+
raise OllamaBudgetExceeded(
|
|
205
|
+
f"Ollama wall-clock budget exceeded: {elapsed:.0f}s > "
|
|
206
|
+
f"{OLLAMA_WALLCLOCK_BUDGET_SECONDS}s. "
|
|
207
|
+
"Raise OLLAMA_WALLCLOCK_BUDGET_SECONDS (env var) if real "
|
|
208
|
+
"workloads legitimately need more than 4 hours per cycle."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _sum_num_turns(state: State, cycle: CycleState | None) -> int:
|
|
213
|
+
"""Sum ``num_turns`` across every phase of the active cycle (or state)."""
|
|
214
|
+
source = cycle.token_usage if cycle is not None else state.token_usage
|
|
215
|
+
return sum(usage.num_turns for usage in source.values())
|
|
@@ -116,6 +116,47 @@ _PHASE1_DEFAULT_MODEL = "claude-opus-4-7"
|
|
|
116
116
|
# CLAUDE.md invariant #8; overridden via ``effective_model`` on Ollama — #219.
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
_PHASE1_MAX_ATTEMPTS = 3
|
|
120
|
+
"""Max planning attempts before surfacing ``Phase1NoIssuesError`` (0.4.12).
|
|
121
|
+
|
|
122
|
+
Weak open models on the Ollama codepath often respond with prose describing
|
|
123
|
+
the plan instead of invoking ``gh issue create`` via the Bash tool. A single
|
|
124
|
+
stricter re-prompt is usually enough to unblock them without operator
|
|
125
|
+
intervention.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
_PHASE1_RETRY_NUDGE = (
|
|
130
|
+
"Your previous attempt finished without creating any GitHub issues. "
|
|
131
|
+
"This is a CRITICAL failure. You MUST use the Bash tool to invoke "
|
|
132
|
+
"`gh issue create` for each issue right now. Do NOT output markdown "
|
|
133
|
+
"or prose describing the plan — every planned issue must become a "
|
|
134
|
+
"real GitHub issue via `gh issue create ... --milestone "
|
|
135
|
+
'"{MILESTONE}" --assignee @me --label "..."`. Once you have called '
|
|
136
|
+
"`gh issue create` for every planned issue, print a one-line summary "
|
|
137
|
+
"per issue and stop.\n\n"
|
|
138
|
+
"The original instructions follow.\n\n"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _accumulate_usage(
|
|
143
|
+
total: _state.TokenUsage | None,
|
|
144
|
+
delta: _state.TokenUsage,
|
|
145
|
+
) -> _state.TokenUsage:
|
|
146
|
+
"""Sum two TokenUsage records field-by-field across Phase 1 attempts."""
|
|
147
|
+
from code_generator.runner.types import TokenUsage
|
|
148
|
+
|
|
149
|
+
if total is None:
|
|
150
|
+
return delta
|
|
151
|
+
return TokenUsage(
|
|
152
|
+
input=total.input + delta.input,
|
|
153
|
+
output=total.output + delta.output,
|
|
154
|
+
cache_read=total.cache_read + delta.cache_read,
|
|
155
|
+
cache_write=total.cache_write + delta.cache_write,
|
|
156
|
+
num_turns=total.num_turns + delta.num_turns,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
119
160
|
def _load_specialized_prompt(
|
|
120
161
|
project_dir: Path,
|
|
121
162
|
state: State,
|
|
@@ -249,28 +290,56 @@ async def run(
|
|
|
249
290
|
**max_turns_kwargs(max_turns),
|
|
250
291
|
)
|
|
251
292
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
293
|
+
# Up to _PHASE1_MAX_ATTEMPTS attempts: the first with the normal
|
|
294
|
+
# prompt, subsequent attempts prefixed with a stricter nudge that
|
|
295
|
+
# tells the model to use the Bash tool to call ``gh issue create``
|
|
296
|
+
# right now. Weak open models on the Ollama codepath commonly
|
|
297
|
+
# respond with prose on the first turn; the nudge recovers most
|
|
298
|
+
# of those cases without operator intervention (0.4.12).
|
|
299
|
+
issue_states: list[_state.IssueState] = []
|
|
300
|
+
total_usage = result = None # type: ignore[assignment]
|
|
301
|
+
attempt_prompt = prompt
|
|
302
|
+
effective_model_name = effective_model or _PHASE1_DEFAULT_MODEL
|
|
303
|
+
for attempt in range(1, _PHASE1_MAX_ATTEMPTS + 1):
|
|
304
|
+
result = await rate_limit.main_loop(
|
|
305
|
+
runner_module,
|
|
306
|
+
attempt_prompt,
|
|
307
|
+
options,
|
|
308
|
+
state_path=state_path,
|
|
309
|
+
logger=logger,
|
|
310
|
+
)
|
|
311
|
+
total_usage = _accumulate_usage(total_usage, result.usage)
|
|
259
312
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
313
|
+
raw_issues = gh.list_issues(
|
|
314
|
+
repo,
|
|
315
|
+
milestone=milestone_title,
|
|
316
|
+
state="all",
|
|
317
|
+
)
|
|
318
|
+
issue_states = _build_issue_states(raw_issues)
|
|
319
|
+
if issue_states:
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if attempt >= _PHASE1_MAX_ATTEMPTS:
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
logger.warning(
|
|
326
|
+
"Phase 1 attempt %d/%d: model returned without creating any "
|
|
327
|
+
"GitHub issues. Re-prompting with a stricter nudge.",
|
|
328
|
+
attempt,
|
|
329
|
+
_PHASE1_MAX_ATTEMPTS,
|
|
330
|
+
)
|
|
331
|
+
nudge = _PHASE1_RETRY_NUDGE.replace("{MILESTONE}", milestone_title or "")
|
|
332
|
+
attempt_prompt = nudge + prompt
|
|
267
333
|
|
|
268
334
|
if not issue_states:
|
|
269
335
|
raise Phase1NoIssuesError(
|
|
270
|
-
"Phase 1 finished without creating any GitHub issues
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
336
|
+
f"Phase 1 finished without creating any GitHub issues after "
|
|
337
|
+
f"{_PHASE1_MAX_ATTEMPTS} attempts (model={effective_model_name!r}). "
|
|
338
|
+
"The model likely responded with prose instead of invoking "
|
|
339
|
+
"`gh issue create` via the Bash tool. Inspect "
|
|
340
|
+
".code-generator/logs/phase1.log for the tool-call trace, "
|
|
341
|
+
"try a stronger model, or simplify the cycle scope in "
|
|
342
|
+
"requirements.md and re-run."
|
|
274
343
|
)
|
|
275
344
|
|
|
276
345
|
target = cycle if cycle is not None else state
|
|
@@ -278,12 +347,17 @@ async def run(
|
|
|
278
347
|
cycle.issues = issue_states
|
|
279
348
|
else:
|
|
280
349
|
state.issues = issue_states
|
|
281
|
-
|
|
350
|
+
# Persist the accumulated usage across all attempts (not just the last).
|
|
351
|
+
target.token_usage["phase1"] = total_usage if total_usage is not None else result.usage
|
|
282
352
|
if hasattr(target, "cache_telemetry"):
|
|
283
|
-
accumulate_telemetry(
|
|
353
|
+
accumulate_telemetry(
|
|
354
|
+
target.cache_telemetry,
|
|
355
|
+
target.token_usage["phase1"],
|
|
356
|
+
result.wall_seconds,
|
|
357
|
+
)
|
|
284
358
|
|
|
285
359
|
_state.save_state(state_path, state)
|
|
286
|
-
log_phase_usage(logger, 1,
|
|
360
|
+
log_phase_usage(logger, 1, target.token_usage["phase1"])
|
|
287
361
|
logger.info("Phase 1: %d issues created.", len(issue_states))
|
|
288
362
|
|
|
289
363
|
except Exception:
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
"""Tests for Ollama per-cycle
|
|
1
|
+
"""Tests for the Ollama per-cycle adaptive safety backstop (issue #220, 0.4.11).
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
As of 0.4.11 the turn counter is a **non-blocking soft warning** — the
|
|
4
|
+
pipeline is never aborted on turn count alone. Real per-call failures are
|
|
5
|
+
the job of :class:`~code_generator.runner.retry.CircuitBreaker` and the
|
|
6
|
+
rate-limit handlers. The wall-clock remains a hard abort (default 4 h).
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
turn count and wall-clock. The Anthropic Max path is untouched.
|
|
8
|
-
|
|
9
|
-
Turn accumulation reads from ``state.token_usage[<phase>].num_turns`` (or
|
|
10
|
-
``cycle.token_usage[<phase>].num_turns`` when a cycle is active), which
|
|
11
|
-
#204/#205 already populate on every phase run.
|
|
8
|
+
Backwards compatibility: ``OLLAMA_TURN_BUDGET`` is preserved as an alias
|
|
9
|
+
for :data:`OLLAMA_SOFT_TURN_WARN` so existing scripts keep importing.
|
|
12
10
|
"""
|
|
13
11
|
|
|
14
12
|
from __future__ import annotations
|
|
15
13
|
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
18
|
from code_generator import state as _state
|
|
19
19
|
from code_generator.orchestrator.ollama_budget import (
|
|
20
|
+
OLLAMA_SOFT_TURN_WARN,
|
|
20
21
|
OLLAMA_TURN_BUDGET,
|
|
21
22
|
OLLAMA_WALLCLOCK_BUDGET_SECONDS,
|
|
22
23
|
OllamaBudgetExceeded,
|
|
@@ -29,12 +30,16 @@ from code_generator.runner.types import TokenUsage
|
|
|
29
30
|
# ---------------------------------------------------------------------------
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class
|
|
33
|
-
def
|
|
34
|
-
assert
|
|
33
|
+
class TestThresholdConstants:
|
|
34
|
+
def test_soft_turn_warn_default_is_500(self) -> None:
|
|
35
|
+
assert OLLAMA_SOFT_TURN_WARN == 500
|
|
35
36
|
|
|
36
|
-
def
|
|
37
|
-
|
|
37
|
+
def test_turn_budget_alias_matches_soft_turn_warn(self) -> None:
|
|
38
|
+
"""Legacy ``OLLAMA_TURN_BUDGET`` must alias the new soft-warn constant."""
|
|
39
|
+
assert OLLAMA_TURN_BUDGET == OLLAMA_SOFT_TURN_WARN
|
|
40
|
+
|
|
41
|
+
def test_wallclock_budget_default_is_4_hours(self) -> None:
|
|
42
|
+
assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == 14400
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
# ---------------------------------------------------------------------------
|
|
@@ -69,69 +74,78 @@ def _make_state_with_usage(
|
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
# ---------------------------------------------------------------------------
|
|
72
|
-
#
|
|
77
|
+
# Soft-turn warning
|
|
73
78
|
# ---------------------------------------------------------------------------
|
|
74
79
|
|
|
75
80
|
|
|
76
|
-
class
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
81
|
+
class TestTurnSoftWarning:
|
|
82
|
+
def test_under_threshold_does_not_warn(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
83
|
+
"""Below the soft threshold → no WARNING emitted, no raise."""
|
|
79
84
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase1": 50, "phase2": 80})
|
|
80
85
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
with caplog.at_level(logging.WARNING):
|
|
88
|
+
tracker.check(st, cycle)
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
turn_warnings = [r for r in caplog.records if "turns" in r.message.lower()]
|
|
91
|
+
assert turn_warnings == []
|
|
92
|
+
|
|
93
|
+
def test_over_threshold_warns_without_raising(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
94
|
+
"""Crossing the soft threshold logs a WARNING; the pipeline continues."""
|
|
95
|
+
st, cycle = _make_state_with_usage(
|
|
96
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
97
|
+
)
|
|
87
98
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
with caplog.at_level(logging.WARNING):
|
|
101
|
+
tracker.check(st, cycle) # must not raise
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
104
|
+
assert len(turn_warnings) == 1
|
|
105
|
+
msg = turn_warnings[0].message
|
|
106
|
+
assert str(OLLAMA_SOFT_TURN_WARN + 100) in msg
|
|
107
|
+
assert str(OLLAMA_SOFT_TURN_WARN) in msg
|
|
108
|
+
|
|
109
|
+
def test_warning_is_emitted_only_once_per_cycle(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
110
|
+
"""Subsequent checks after the first warning must stay silent."""
|
|
111
|
+
st, cycle = _make_state_with_usage(
|
|
112
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
113
|
+
)
|
|
94
114
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
95
115
|
|
|
96
|
-
with
|
|
116
|
+
with caplog.at_level(logging.WARNING):
|
|
117
|
+
tracker.check(st, cycle)
|
|
118
|
+
tracker.check(st, cycle)
|
|
97
119
|
tracker.check(st, cycle)
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
assert
|
|
101
|
-
assert str(OLLAMA_TURN_BUDGET) in msg
|
|
102
|
-
assert str(OLLAMA_TURN_BUDGET + 1) in msg
|
|
121
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
122
|
+
assert len(turn_warnings) == 1
|
|
103
123
|
|
|
104
|
-
def test_turns_aggregate_across_phases(self) -> None:
|
|
105
|
-
"""num_turns from every phase sum toward the
|
|
124
|
+
def test_turns_aggregate_across_phases(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
125
|
+
"""num_turns from every phase sum toward the soft threshold."""
|
|
106
126
|
st, cycle = _make_state_with_usage(
|
|
107
127
|
{},
|
|
108
128
|
cycle_turns={
|
|
109
|
-
"phase0":
|
|
110
|
-
"phase1":
|
|
111
|
-
"phase2":
|
|
112
|
-
"phase3_4":
|
|
113
|
-
"phase5":
|
|
114
|
-
"phase6":
|
|
115
|
-
"phase7":
|
|
129
|
+
"phase0": 100,
|
|
130
|
+
"phase1": 80,
|
|
131
|
+
"phase2": 80,
|
|
132
|
+
"phase3_4": 200,
|
|
133
|
+
"phase5": 60,
|
|
134
|
+
"phase6": 20,
|
|
135
|
+
"phase7": 10,
|
|
116
136
|
},
|
|
117
137
|
)
|
|
118
|
-
# Sum =
|
|
138
|
+
# Sum = 550 > 500.
|
|
119
139
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
120
140
|
|
|
121
|
-
with
|
|
122
|
-
tracker.check(st, cycle)
|
|
123
|
-
|
|
124
|
-
def test_single_mode_uses_state_token_usage_when_cycle_is_none(self) -> None:
|
|
125
|
-
"""When ``cycle`` is None, total turns come from state.token_usage."""
|
|
126
|
-
st, _c = _make_state_with_usage({"phase0": OLLAMA_TURN_BUDGET + 5})
|
|
127
|
-
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
141
|
+
with caplog.at_level(logging.WARNING):
|
|
142
|
+
tracker.check(st, cycle) # must not raise
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
tracker.check(st, cycle=None)
|
|
144
|
+
assert any("550" in r.message for r in caplog.records)
|
|
131
145
|
|
|
132
146
|
|
|
133
147
|
# ---------------------------------------------------------------------------
|
|
134
|
-
# Wall-clock
|
|
148
|
+
# Wall-clock hard abort
|
|
135
149
|
# ---------------------------------------------------------------------------
|
|
136
150
|
|
|
137
151
|
|
|
@@ -145,7 +159,6 @@ class TestWallclockBudget:
|
|
|
145
159
|
clock=lambda: now,
|
|
146
160
|
)
|
|
147
161
|
tracker.start()
|
|
148
|
-
# Simulate time passage below the budget.
|
|
149
162
|
now += OLLAMA_WALLCLOCK_BUDGET_SECONDS - 10
|
|
150
163
|
|
|
151
164
|
tracker.check(st, cycle) # must not raise
|
|
@@ -153,7 +166,6 @@ class TestWallclockBudget:
|
|
|
153
166
|
def test_over_budget_raises_with_exact_message(self) -> None:
|
|
154
167
|
"""Elapsed > budget → OllamaBudgetExceeded naming wall-clock."""
|
|
155
168
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
156
|
-
# Use a small mutable clock to simulate time moving forward.
|
|
157
169
|
t = [1_000_000.0]
|
|
158
170
|
|
|
159
171
|
def _clock() -> float:
|
|
@@ -175,22 +187,36 @@ class TestWallclockBudget:
|
|
|
175
187
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
176
188
|
tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: 1e12)
|
|
177
189
|
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
tracker.check(st, cycle) # must not raise
|
|
191
|
+
|
|
192
|
+
def test_extreme_turn_count_does_not_raise(self) -> None:
|
|
193
|
+
"""No amount of turns should raise on its own — only wall-clock hard-aborts."""
|
|
194
|
+
st, cycle = _make_state_with_usage(
|
|
195
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN * 100}
|
|
196
|
+
)
|
|
197
|
+
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
198
|
+
|
|
199
|
+
tracker.check(st, cycle) # must not raise
|
|
180
200
|
|
|
181
201
|
|
|
182
202
|
# ---------------------------------------------------------------------------
|
|
183
|
-
# Anthropic Max path —
|
|
203
|
+
# Anthropic Max path — thresholds do not fire
|
|
184
204
|
# ---------------------------------------------------------------------------
|
|
185
205
|
|
|
186
206
|
|
|
187
207
|
class TestAnthropicMaxUntouched:
|
|
188
|
-
def
|
|
189
|
-
"""provider_is_ollama=False →
|
|
190
|
-
st, cycle = _make_state_with_usage(
|
|
208
|
+
def test_anthropic_max_mode_skips_turn_warning(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
209
|
+
"""provider_is_ollama=False → no WARNING, no raise."""
|
|
210
|
+
st, cycle = _make_state_with_usage(
|
|
211
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
212
|
+
)
|
|
191
213
|
tracker = OllamaBudgetTracker(provider_is_ollama=False)
|
|
192
214
|
|
|
193
|
-
|
|
215
|
+
with caplog.at_level(logging.WARNING):
|
|
216
|
+
tracker.check(st, cycle)
|
|
217
|
+
|
|
218
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
219
|
+
assert turn_warnings == []
|
|
194
220
|
|
|
195
221
|
def test_anthropic_max_mode_skips_wallclock_budget(self) -> None:
|
|
196
222
|
"""provider_is_ollama=False → wall-clock breach does not raise."""
|