god-code 1.0.0__tar.gz → 1.0.1__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.
- {god_code-1.0.0 → god_code-1.0.1}/CHANGELOG.md +71 -0
- {god_code-1.0.0 → god_code-1.0.1}/PKG-INFO +1 -1
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/dispatcher.py +47 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/commands.py +87 -14
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/engine_wiring.py +5 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/config.py +4 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/context_manager.py +18 -3
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/engine.py +260 -11
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/base.py +38 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/godot_cli.py +13 -1
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/image_gen.py +11 -2
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/screenshot.py +15 -3
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/display.py +30 -0
- {god_code-1.0.0 → god_code-1.0.1}/pyproject.toml +1 -1
- {god_code-1.0.0 → god_code-1.0.1}/tests/agents/test_dispatcher.py +177 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_config.py +18 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_context_manager.py +72 -0
- god_code-1.0.1/tests/runtime/test_engine.py +1173 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_multi_agent_flow.py +5 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_cli_interactive_flows.py +13 -1
- {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_display.py +55 -0
- god_code-1.0.0/tests/runtime/test_engine.py +0 -495
- {god_code-1.0.0 → god_code-1.0.1}/.codetape/component-map.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.codetape/config.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.github/FUNDING.yml +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.github/workflows/docs.yml +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.github/workflows/publish.yml +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.gitignore +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/.gitleaks.toml +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/AGENTS.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/CLAUDE.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/CONTRIBUTING.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/LICENSE +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/PRIVACY.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/README.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/SECURITY.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/cli/ask.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/cli/chat.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/empty-project-to-first-verified-change.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/gameplay-intent-and-enemy-ai-roadmap.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/getting-started/first-run.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/getting-started/install.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/index.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/mcp/overview.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-api-backend-design.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-api-backend-impl.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-bullet-hell-sprite-qa-demo-polish-backlog.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-demo-ready-upgrade.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-dogfooding-inventory.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-v08-design.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-v08-impl.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-work-status.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-06-auto-flow-impl.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-06-interactive-ux-redesign.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit-design.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit-report.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-v1.0.0-ux-upgrade-design.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/providers/byok-and-oauth.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/tui/menu-and-commands.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/tui/settings-and-byok.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/docs/validation/quality-gate.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/addons/god_code_bridge/god_code_bridge.gd +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/addons/god_code_bridge/plugin.cfg +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/configs.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/results.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/__main__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/helpers.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/menus.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/entrypoint.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/audio_scaffolder.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/collision_planner.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/consistency_checker.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/dependency_graph.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/gdscript_linter.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/impact_analysis.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/pattern_advisor.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/project.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/resource_validator.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/scene_parser.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/scene_writer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/tscn_validator.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/ui_layout_advisor.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/variant_codec.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/anthropic.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/base.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/openai.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/client.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/redact.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/streaming.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/types.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/vision.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/mcp_server.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/assembler.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/build_discipline.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/genre_templates.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/godot_playbook.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/image_templates.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/knowledge_selector.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/skill_library.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/skill_selector.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/system.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/vision_templates.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/py.typed +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/auth.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/context_health.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/design_memory.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/error_loop.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/events.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/execution_plan.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/gameplay_reviewer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/intent_resolver.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/live_client.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/modes.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/oauth.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/playtest_harness.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/polish_rubric.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/providers.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/quality_gate.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/reviewer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/runtime_bridge.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_pattern_readability.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_phase_transition.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_wave_progression.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_boss_transition.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_combat_feedback.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_ui_readability.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_wave_pacing.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/hud_feedback.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/platformer_enemy_gap_jump.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/platformer_enemy_patrol_response.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/player_movement.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/scene_transition.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/topdown_shooter_flank_resolution.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/topdown_shooter_pressure.json +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/session.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/validation_checks.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/visual_regression.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/classifier.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/hooks.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/policies.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/protected_paths.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/tool_pipeline.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/testing/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/testing/scenario_runner.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/analysis_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/editor_bridge.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/file_ops.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/git.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/list_dir.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/memory_tool.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/registry.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/runtime_harness.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/scene_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/script_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/search.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/shell.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/sprite_pipeline.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/sprite_qa.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/vision_analysis.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/vision_scoring.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/web_search.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/input_handler.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/mkdocs.yml +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/scripts/install-hooks.sh +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/scripts/vision_model_comparison.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/skills/god-code-setup/SKILL.md +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/agents/test_playtest_analyst.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/cli/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/cli/test_setup_bridge.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/cli_test_utils.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_planner_worker_reviewer_flow.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_policy_enforcement.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_scenario_runner.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/fuzz/test_input_sequences.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_audio_scaffolder.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_collision_planner.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_consistency.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_dependency_graph.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_impact_analysis.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_linter.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_pattern_advisor.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_project.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_resource_validator.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_scene_parser.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_scene_writer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_tscn_validator.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_ui_layout_advisor.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_variant_codec.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/llm/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_adapters.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_client.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_streaming.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_vision.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_genre_templates.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_knowledge_selector.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_prompt_assembler.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_skill_selector.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_system_prompt.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_vision_templates.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_context_health.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_design_memory.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_error_loop.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_execution_plan.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_gameplay_reviewer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_intent_resolver.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_live_client.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_mode_restrictions.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_playtest_harness.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_quality_gate.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_reviewer.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_runtime_bridge.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_session.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_validation_checks.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_visual_regression.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_classifier.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_hooks.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_permissions.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_tool_pipeline.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_auto_flow.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_cli_config_flow.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_e2e.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_package_compatibility.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/test_runtime_switch_commands.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_analysis_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_editor_bridge.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_file_ops.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_git.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_godot_cli.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_list_dir.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_memory_tool.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_registry.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_runtime_harness.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_scene_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_screenshot_tool.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_script_tools.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_search.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_shell.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_sprite_qa.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_vision_analysis.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_vision_scoring.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tui/__init__.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_input_handler.py +0 -0
- {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_plan_display.py +0 -0
|
@@ -2,6 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to God Code will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.1] — 2026-04-07
|
|
6
|
+
|
|
7
|
+
**Token-efficiency + cancellation closeout.** This release attacks the dominant token cost driver on long dev sessions (planner blocks accumulating in history and rebilling on every subsequent LLM call) and completes the v1.0.0 deferred items (full asyncio.Task cancellation, subprocess termination, per-tool progress events).
|
|
8
|
+
|
|
9
|
+
**No breaking API changes.** Config additions are opt-out (defaults preserve the new behavior). Event taxonomy gains three additive kinds (`tool_progress`, `plan_pruned`, `planner_skipped`). All 681 v1.0.0 tests continue to pass; 58 new regression tests added (739 total).
|
|
10
|
+
|
|
11
|
+
### Token efficiency (the main motivator)
|
|
12
|
+
|
|
13
|
+
- **T1: Planner blocks pruned from history** — `_maybe_run_planner` now calls `prune_system_reports` (a helper that existed and was tested but never wired into production code) to cap plan history at 2 blocks (configurable via `plan_history_keep`). On a 40-turn dev session, this cuts planner-attributable rebill from ~410K tokens to ~2K tokens — roughly $1.2/session saved at gpt-5.4 input rates. Helper extended with an additive `prefix_filter` parameter so pruning is scoped to `"[SYSTEM] Planner pass"` and cannot accidentally drop quality-gate or reviewer reports.
|
|
14
|
+
|
|
15
|
+
- **T2: Lazy planner triggering** — `_maybe_run_planner` now calls a new `should_run_planner(user_input)` heuristic first. Trivial read-only inputs (`explain X`, `show Y`, `what is in Z`, `如何`, `解釋`, `顯示`, `為什麼`, question marks with `什麼`) skip the full planner cost. Action verbs (EN: implement/add/fix/refactor/... | 繁中: 實作/新增/修/改/重寫) or multi-file references still trigger a plan. Config opt-out: `planner_lazy: false` restores the v1.0.0 unconditional behavior. Emits a new `planner_skipped` event with the reason for dogfood observability.
|
|
16
|
+
|
|
17
|
+
- **T3: Plan content shaping** — Added `extract_worker_plan()` in `agents/dispatcher.py`. The `[SYSTEM] Planner pass` block injected into main history now contains only the actionable subset (Goal + Steps), not the full 5-section markdown. Scope + Risks + Validation are user-facing context already streamed to the TUI via the planner sub-engine, so they don't need to be re-injected. Typical saving: ~30% of per-plan injected tokens. Defensive fallback: returns the full text unchanged if the LLM produces malformed markdown.
|
|
18
|
+
|
|
19
|
+
- **T4: `prune_system_reports` wired up** — the helper shipped in v0.x with tests but was dead code. Now in use per T1 above.
|
|
20
|
+
|
|
21
|
+
### Cancellation closeout (v1.0.0 deferred)
|
|
22
|
+
|
|
23
|
+
- **D1: Full asyncio.Task cancellation in chat loop** — Completes the v1.0.0/C2 partial implementation. New `_submit_cancellable` helper in `cli/commands.py` wraps `engine.submit()` in `asyncio.create_task`, catches `CancelledError`/`KeyboardInterrupt`, cancels the task, runs cleanup, and raises a new `TurnCancelled` exception (used instead of re-raising `KeyboardInterrupt` because `KeyboardInterrupt` inside an asyncio task trips pytest-asyncio's BaseException handling). Shared cleanup in `_cleanup_after_cancel(engine)` guarded with `hasattr` for FakeEngine compat.
|
|
24
|
+
|
|
25
|
+
- **D2: Subprocess registry + termination** — New contextvar-based subprocess registry on `ConversationEngine`. `register_subprocess(proc)` / `unregister_subprocess(proc)` / `async terminate_active_subprocesses(timeout=2.0)`. `terminate_active_subprocesses` sends SIGTERM, waits up to 2s, then escalates to SIGKILL for stubborn processes. Activated during `submit()` / `submit_with_images()` via contextvar. Tools lookup the active registry via `get_current_subprocess_registry()`. `tools/godot_cli.run_godot_command` and `tools/screenshot.CaptureScreenshotTool` register their subprocesses on spawn. Ctrl+C during a Godot validate or screenshot capture now kills the subprocess within 2s instead of letting it run to natural completion (30-120s wasted).
|
|
26
|
+
|
|
27
|
+
- **D3: Per-tool progress events** — Completes v1.0.0/A2 Layer 2. New `tool_progress` event kind + `tools/base.py::emit_tool_progress(context, ...)` helper. `generate_sprite` emits 5 phases (API call → post-process → save → QA → reimport) so the spinner shows `generate_sprite: post-processing (2/5)` instead of a static label. TUI display handler updates the active status spinner's label dynamically.
|
|
28
|
+
|
|
29
|
+
### Measured impact (design-doc projection)
|
|
30
|
+
|
|
31
|
+
| Metric | v1.0.0 | v1.0.1 | Delta |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| Planner-attributable rebill (40-turn session) | ~430K tokens | ~18K tokens | **-96%** |
|
|
34
|
+
| Plan history carried (40-turn session) | ~20K tokens | ~700 tokens | **-96%** |
|
|
35
|
+
| Whole-session input tokens (conservative) | baseline | baseline × 0.70-0.92 | **-8% to -30%** |
|
|
36
|
+
| Ctrl+C latency (worst case subprocess) | 30-120s | < 2s | **-95%** |
|
|
37
|
+
|
|
38
|
+
Actual impact will be measured over the first dogfood session on v1.0.1.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `prune_system_reports` `prefix_filter` parameter (scoped pruning)
|
|
43
|
+
- `should_run_planner(user_input)` heuristic
|
|
44
|
+
- `extract_worker_plan(plan_text)` function in dispatcher
|
|
45
|
+
- `plan_history_keep: int = 2` config field
|
|
46
|
+
- `planner_lazy: bool = True` config field
|
|
47
|
+
- `ConversationEngine._active_subprocesses` set
|
|
48
|
+
- `register_subprocess` / `unregister_subprocess` / `terminate_active_subprocesses` engine methods
|
|
49
|
+
- `_activate_subprocess_registry` / `_deactivate_subprocess_registry` engine methods
|
|
50
|
+
- `get_current_subprocess_registry()` module-level accessor
|
|
51
|
+
- `SubprocessRegistryProtocol` typing protocol
|
|
52
|
+
- `TurnCancelled(Exception)` in `cli/commands.py`
|
|
53
|
+
- `_cleanup_after_cancel(engine)` async helper in `cli/commands.py`
|
|
54
|
+
- `_submit_cancellable(engine, user_input, cfg, display)` async helper in `cli/commands.py`
|
|
55
|
+
- `emit_tool_progress(context, ...)` helper in `tools/base.py`
|
|
56
|
+
- New event kinds: `plan_pruned`, `planner_skipped`, `tool_progress`
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- `_maybe_run_planner`: now checks `should_run_planner` when `planner_lazy=True`, calls `extract_worker_plan` on the planner output before injection, and calls `prune_system_reports` after appending
|
|
61
|
+
- `submit()` / `submit_with_images()`: wrap work in `_activate_subprocess_registry` / `_deactivate_subprocess_registry` context
|
|
62
|
+
- `tools/godot_cli.run_godot_command`: registers subprocess with active registry
|
|
63
|
+
- `tools/screenshot.CaptureScreenshotTool.execute`: registers subprocess with active registry
|
|
64
|
+
- Chat loop (`cli/commands.py`): uses `_submit_cancellable` instead of bare `await engine.submit`
|
|
65
|
+
|
|
66
|
+
### Test count
|
|
67
|
+
|
|
68
|
+
681 → 739 (+58 regression tests)
|
|
69
|
+
|
|
70
|
+
### Known scope notes
|
|
71
|
+
|
|
72
|
+
- `image_gen` single-candidate API call — the design doc mentioned "4 candidates" but the current `GenerateSpriteTool` requests `n=1` from the image API. Per-tool progress is instead emitted as 5 execution phases (API call, post-process, save, QA, reimport) which covers the full wall-clock.
|
|
73
|
+
- `_run_visual_iteration` engine method doesn't exist as a standalone — the vision loop is LLM-driven via tool calls, not a hardcoded counter. `tool_progress` instrumentation for vision iteration was dropped from this release; the user-visible stage updates come from the individual tool-level progress (screenshot, analyze, score) rather than a synthetic "iteration N/3" counter.
|
|
74
|
+
- Windows cancellation: `asyncio.CancelledError` handling path works cross-platform. The subprocess SIGTERM/SIGKILL path is POSIX-only but macOS/Linux is the current supported platform set. Windows gets best-effort behavior via normal Python subprocess termination.
|
|
75
|
+
|
|
5
76
|
## [1.0.0] — 2026-04-07
|
|
6
77
|
|
|
7
78
|
**Promoted from 1.0.0rc1 with no code changes** after Starfall workflow dogfood verification. See the `[1.0.0rc1]` section below for the full list of fixes and design rationale.
|
|
@@ -2,10 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from dataclasses import replace
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Callable
|
|
8
9
|
|
|
10
|
+
|
|
11
|
+
def extract_worker_plan(plan_text: str) -> str:
|
|
12
|
+
"""Reduce a full planner output to just the actionable subset.
|
|
13
|
+
|
|
14
|
+
The planner prompt (v1.0.0/F2) enforces a 5-section markdown format:
|
|
15
|
+
Goal / Scope / Steps / Risks / Validation. The worker sub-agent and
|
|
16
|
+
the main engine only need Goal + Steps to execute correctly — Scope,
|
|
17
|
+
Risks, and Validation are user-facing context that the TUI already
|
|
18
|
+
shows separately. Injecting them into main history wastes tokens on
|
|
19
|
+
every subsequent LLM call (v1.0.1/T3).
|
|
20
|
+
|
|
21
|
+
Returns the reduced plan. Defensive fallback: if the markdown can't
|
|
22
|
+
be parsed (malformed LLM output), returns the input unchanged — we
|
|
23
|
+
never want the worker to lose its plan entirely because of a parser
|
|
24
|
+
mismatch.
|
|
25
|
+
|
|
26
|
+
Empty input returns empty string.
|
|
27
|
+
"""
|
|
28
|
+
if not plan_text:
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
# Try to find the **Goal**: and **Steps**: sections. If either is
|
|
32
|
+
# missing, we're dealing with malformed output — fall back to the
|
|
33
|
+
# full text.
|
|
34
|
+
goal_match = re.search(
|
|
35
|
+
r"\*\*Goal\*\*\s*:\s*(.+?)(?=\n\s*\*\*|$)",
|
|
36
|
+
plan_text,
|
|
37
|
+
re.DOTALL,
|
|
38
|
+
)
|
|
39
|
+
steps_match = re.search(
|
|
40
|
+
r"\*\*Steps\*\*\s*:\s*\n(.+?)(?=\n\s*\*\*(?:Risks|Validation)\*\*|$)",
|
|
41
|
+
plan_text,
|
|
42
|
+
re.DOTALL,
|
|
43
|
+
)
|
|
44
|
+
if not goal_match or not steps_match:
|
|
45
|
+
return plan_text # defensive fallback
|
|
46
|
+
|
|
47
|
+
goal = goal_match.group(1).strip()
|
|
48
|
+
steps = steps_match.group(1).strip()
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
"## Plan\n\n"
|
|
52
|
+
f"**Goal**: {goal}\n\n"
|
|
53
|
+
f"**Steps**:\n{steps}\n"
|
|
54
|
+
)
|
|
55
|
+
|
|
9
56
|
from godot_agent.godot.impact_analysis import format_impact_report, infer_request_impact
|
|
10
57
|
from godot_agent.agents.configs import AGENT_CONFIGS, AgentConfig
|
|
11
58
|
from godot_agent.agents.results import AgentTaskResult
|
|
@@ -537,6 +537,76 @@ def ask(prompt: str, project: str, config: str | None, image: tuple[str, ...], p
|
|
|
537
537
|
click.echo(result)
|
|
538
538
|
|
|
539
539
|
|
|
540
|
+
class TurnCancelled(Exception):
|
|
541
|
+
"""Raised by :func:`_submit_cancellable` when the user cancels mid-turn.
|
|
542
|
+
|
|
543
|
+
Used as an internal signal between the helper and the chat loop so
|
|
544
|
+
pytest-asyncio can catch cancellation via ``pytest.raises`` without
|
|
545
|
+
tripping over KeyboardInterrupt / CancelledError's special handling
|
|
546
|
+
inside an event loop. The chat loop translates this back into a
|
|
547
|
+
"Cancelled" message and continues the session.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
async def _cleanup_after_cancel(engine) -> None:
|
|
552
|
+
"""Shared cleanup for the cancellation path.
|
|
553
|
+
|
|
554
|
+
Calls ``engine.terminate_active_subprocesses()`` (v1.0.1/D2) to
|
|
555
|
+
SIGTERM + SIGKILL any in-flight subprocesses, then
|
|
556
|
+
``engine.rollback_current_turn()`` (v1.0.0/C2) to drop any messages
|
|
557
|
+
appended during the cancelled turn. Both are guarded with hasattr
|
|
558
|
+
for FakeEngine compatibility.
|
|
559
|
+
"""
|
|
560
|
+
if hasattr(engine, "terminate_active_subprocesses"):
|
|
561
|
+
try:
|
|
562
|
+
await engine.terminate_active_subprocesses()
|
|
563
|
+
except Exception:
|
|
564
|
+
pass
|
|
565
|
+
if hasattr(engine, "rollback_current_turn"):
|
|
566
|
+
try:
|
|
567
|
+
engine.rollback_current_turn()
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
async def _submit_cancellable(engine, user_input: str, cfg, display) -> str:
|
|
573
|
+
"""Submit a user input as a cancellable asyncio task.
|
|
574
|
+
|
|
575
|
+
Wraps ``engine.submit()`` in ``asyncio.create_task`` so that a
|
|
576
|
+
KeyboardInterrupt (Ctrl+C) during the await can cancel the task,
|
|
577
|
+
terminate any tool-spawned subprocesses, and roll back the turn's
|
|
578
|
+
message state — completing the v1.0.0/C2 partial fix with the full
|
|
579
|
+
cancellation architecture planned in v1.0.1/D1.
|
|
580
|
+
|
|
581
|
+
On success: returns the assistant response string.
|
|
582
|
+
|
|
583
|
+
On KeyboardInterrupt / CancelledError: cancels the task, awaits it
|
|
584
|
+
to drain, runs cleanup via ``_cleanup_after_cancel``, then raises
|
|
585
|
+
``TurnCancelled`` so the chat loop can catch it and continue the
|
|
586
|
+
session. Uses a custom exception type (not KeyboardInterrupt) to
|
|
587
|
+
avoid pytest-asyncio / event loop special handling of BaseException.
|
|
588
|
+
"""
|
|
589
|
+
submit_task = asyncio.create_task(engine.submit(user_input))
|
|
590
|
+
try:
|
|
591
|
+
if cfg.streaming and engine.on_stream_chunk:
|
|
592
|
+
return await submit_task
|
|
593
|
+
else:
|
|
594
|
+
with display.thinking():
|
|
595
|
+
result = await submit_task
|
|
596
|
+
display.agent_response(result)
|
|
597
|
+
return result
|
|
598
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
599
|
+
submit_task.cancel()
|
|
600
|
+
try:
|
|
601
|
+
await submit_task
|
|
602
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
603
|
+
pass
|
|
604
|
+
except Exception:
|
|
605
|
+
pass
|
|
606
|
+
await _cleanup_after_cancel(engine)
|
|
607
|
+
raise TurnCancelled()
|
|
608
|
+
|
|
609
|
+
|
|
540
610
|
@main.command()
|
|
541
611
|
@click.option("--project", "-p", default=".", help="Path to Godot project root")
|
|
542
612
|
@click.option("--config", "-c", default=None, help="Path to config file")
|
|
@@ -1775,21 +1845,24 @@ def chat(project: str = ".", config: str | None = None):
|
|
|
1775
1845
|
await _edit_intent_profile(checkpoint=True)
|
|
1776
1846
|
engine.refresh_intent_profile(user_input)
|
|
1777
1847
|
_refresh_workspace()
|
|
1778
|
-
# v1.0.
|
|
1779
|
-
#
|
|
1780
|
-
#
|
|
1781
|
-
#
|
|
1782
|
-
#
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
display.agent_response(response)
|
|
1789
|
-
except KeyboardInterrupt:
|
|
1848
|
+
# v1.0.1/D1: fully-cancellable submit. Wraps
|
|
1849
|
+
# engine.submit() in asyncio.create_task so Ctrl+C
|
|
1850
|
+
# can cancel the task, terminate any spawned
|
|
1851
|
+
# subprocesses, and roll back the turn's message
|
|
1852
|
+
# state (completes v1.0.0/C2). See
|
|
1853
|
+
# _submit_cancellable docstring for details.
|
|
1854
|
+
response = await _submit_cancellable(
|
|
1855
|
+
engine, user_input, cfg, display
|
|
1856
|
+
)
|
|
1857
|
+
except (KeyboardInterrupt, TurnCancelled):
|
|
1790
1858
|
display.info("Cancelled")
|
|
1791
|
-
|
|
1792
|
-
|
|
1859
|
+
# _submit_cancellable already ran cleanup via
|
|
1860
|
+
# _cleanup_after_cancel, but if the exception was a
|
|
1861
|
+
# raw KeyboardInterrupt (e.g. during the
|
|
1862
|
+
# refresh_intent_profile or _edit_intent_profile
|
|
1863
|
+
# phase before submit started), run cleanup
|
|
1864
|
+
# defensively to ensure no partial state leaks.
|
|
1865
|
+
await _cleanup_after_cancel(engine)
|
|
1793
1866
|
continue
|
|
1794
1867
|
except httpx.HTTPStatusError as e:
|
|
1795
1868
|
status_code = e.response.status_code if e.response is not None else "?"
|
|
@@ -334,6 +334,11 @@ def _wire_engine_callbacks(
|
|
|
334
334
|
engine.on_event = display.handle_event
|
|
335
335
|
engine.auto_commit = cfg.auto_commit
|
|
336
336
|
engine.use_streaming = cfg.streaming
|
|
337
|
+
# v1.0.1/T1+T2: propagate token-efficiency settings from config.
|
|
338
|
+
# getattr defensively so CLI flow tests that construct configs
|
|
339
|
+
# without these fields continue to work.
|
|
340
|
+
engine.planner_lazy = getattr(cfg, "planner_lazy", True)
|
|
341
|
+
engine.plan_history_keep = getattr(cfg, "plan_history_keep", 2)
|
|
337
342
|
if cfg.streaming:
|
|
338
343
|
engine.on_stream_start = display.agent_streaming_start
|
|
339
344
|
engine.on_stream_chunk = display.agent_streaming_chunk
|
|
@@ -46,6 +46,10 @@ class AgentConfig(BaseModel):
|
|
|
46
46
|
enabled_skills: list[str] = Field(default_factory=list)
|
|
47
47
|
disabled_skills: list[str] = Field(default_factory=list)
|
|
48
48
|
|
|
49
|
+
# Token efficiency (v1.0.1)
|
|
50
|
+
planner_lazy: bool = True # Skip planner on trivial read-only turns (T2)
|
|
51
|
+
plan_history_keep: int = 2 # How many planner blocks to keep in history (T1)
|
|
52
|
+
|
|
49
53
|
# Backend orchestration
|
|
50
54
|
backend_url: str = "" # Empty = direct provider (current behavior)
|
|
51
55
|
backend_cost_preference: str = "balanced" # economy | balanced | quality
|
|
@@ -241,11 +241,26 @@ def truncate_tool_result(content: str, max_chars: int = 2000) -> str:
|
|
|
241
241
|
return f"{content[:head_size]}\n[...truncated {omitted} chars...]\n{content[-tail_size:]}"
|
|
242
242
|
|
|
243
243
|
|
|
244
|
-
def prune_system_reports(
|
|
245
|
-
|
|
244
|
+
def prune_system_reports(
|
|
245
|
+
messages: list[Message],
|
|
246
|
+
max_reports: int = 2,
|
|
247
|
+
prefix_filter: str | None = None,
|
|
248
|
+
) -> list[Message]:
|
|
249
|
+
"""Remove old [SYSTEM] reports from history, keeping the latest N.
|
|
250
|
+
|
|
251
|
+
If ``prefix_filter`` is set, only messages whose content starts with the
|
|
252
|
+
given prefix are considered for pruning. Other [SYSTEM]-prefixed messages
|
|
253
|
+
pass through untouched. This lets callers scope the prune to a specific
|
|
254
|
+
report type (e.g. ``"[SYSTEM] Planner"``) without collateral damage to
|
|
255
|
+
quality-gate or reviewer reports.
|
|
256
|
+
|
|
257
|
+
Backward compat: when ``prefix_filter`` is None, every [SYSTEM]-prefixed
|
|
258
|
+
message is eligible for pruning (original v1.0.0 behavior).
|
|
259
|
+
"""
|
|
260
|
+
match_prefix = prefix_filter if prefix_filter is not None else "[SYSTEM]"
|
|
246
261
|
report_indices: list[int] = []
|
|
247
262
|
for i, m in enumerate(messages):
|
|
248
|
-
if m.role == "user" and isinstance(m.content, str) and m.content.startswith(
|
|
263
|
+
if m.role == "user" and isinstance(m.content, str) and m.content.startswith(match_prefix):
|
|
249
264
|
report_indices.append(i)
|
|
250
265
|
if len(report_indices) <= max_reports:
|
|
251
266
|
return messages
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextvars
|
|
5
7
|
import enum
|
|
6
8
|
import json
|
|
7
9
|
import logging
|
|
8
10
|
from dataclasses import dataclass, field
|
|
9
11
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol
|
|
11
13
|
|
|
12
14
|
from godot_agent.godot.impact_analysis import ImpactAnalysisReport, analyze_change_impact
|
|
13
15
|
from godot_agent.llm.client import LLMClient, Message, TokenUsage
|
|
14
16
|
from godot_agent.prompts.assembler import PromptAssembler
|
|
15
17
|
from godot_agent.prompts.skill_selector import narrow_tools_for_skills, resolve_skills
|
|
16
|
-
from godot_agent.runtime.context_manager import smart_compact, estimate_message_tokens, truncate_tool_result, compress_step_messages
|
|
18
|
+
from godot_agent.runtime.context_manager import smart_compact, estimate_message_tokens, truncate_tool_result, compress_step_messages, prune_system_reports
|
|
17
19
|
from godot_agent.runtime.execution_plan import ExecutionPlan, PlanStep
|
|
18
20
|
from godot_agent.runtime.design_memory import DesignMemory, GameplayIntentProfile, load_design_memory
|
|
19
21
|
from godot_agent.runtime.events import EngineEvent
|
|
@@ -38,6 +40,125 @@ log = logging.getLogger(__name__)
|
|
|
38
40
|
|
|
39
41
|
# Compact at 75% of 1.05M context to leave room for current turn
|
|
40
42
|
_COMPACT_THRESHOLD = 787500 # 75% of 1.05M
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# v1.0.1/D2: subprocess registry context variable. Tools that spawn
|
|
46
|
+
# subprocesses can look up the current engine's registry via
|
|
47
|
+
# get_current_subprocess_registry() and register their process so the
|
|
48
|
+
# engine can terminate it on Ctrl+C. Scoped per-asyncio-task so
|
|
49
|
+
# concurrent engines (e.g. in tests) don't collide.
|
|
50
|
+
_current_subprocess_registry: contextvars.ContextVar[
|
|
51
|
+
"SubprocessRegistryProtocol | None"
|
|
52
|
+
] = contextvars.ContextVar("_current_subprocess_registry", default=None)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SubprocessRegistryProtocol(Protocol):
|
|
56
|
+
"""Minimal interface a subprocess registry must expose. In practice
|
|
57
|
+
this is ConversationEngine but tests and alternative runtimes can
|
|
58
|
+
provide their own."""
|
|
59
|
+
def register_subprocess(self, proc: Any) -> None: ...
|
|
60
|
+
def unregister_subprocess(self, proc: Any) -> None: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_current_subprocess_registry() -> "SubprocessRegistryProtocol | None":
|
|
64
|
+
"""Return the currently-active subprocess registry, or None if
|
|
65
|
+
called outside an engine's submit() scope. Tools use this to opt
|
|
66
|
+
into cancellable subprocess execution without having the engine
|
|
67
|
+
passed through every call."""
|
|
68
|
+
return _current_subprocess_registry.get()
|
|
69
|
+
|
|
70
|
+
# v1.0.1/T2: lazy planner heuristic — action verbs trigger a plan pass,
|
|
71
|
+
# read-only question words skip it. Kept as module-level frozen sets so
|
|
72
|
+
# should_run_planner can't mutate them across calls.
|
|
73
|
+
_ACTION_VERBS_EN = frozenset({
|
|
74
|
+
"implement", "add", "create", "build", "fix", "refactor", "rewrite",
|
|
75
|
+
"change", "update", "delete", "remove", "generate", "migrate",
|
|
76
|
+
"rename", "extract", "split", "merge", "introduce", "replace",
|
|
77
|
+
})
|
|
78
|
+
_ACTION_VERBS_ZH = frozenset({
|
|
79
|
+
"實作", "新增", "建立", "修", "重寫", "改", "刪除", "產生", "遷移",
|
|
80
|
+
"重構", "替換", "抽出", "合併", "引入", "更新", "加入", "加上",
|
|
81
|
+
})
|
|
82
|
+
_READONLY_INTENTS_EN = frozenset({
|
|
83
|
+
"what", "why", "how", "where", "which", "who", "when",
|
|
84
|
+
"show", "explain", "read", "list", "find", "describe", "display",
|
|
85
|
+
"print", "tell", "inspect", "view",
|
|
86
|
+
})
|
|
87
|
+
_READONLY_INTENTS_ZH = frozenset({
|
|
88
|
+
"為什麼", "如何", "顯示", "解釋", "讀", "列", "描述", "查看",
|
|
89
|
+
"印出", "看一下", "說明", "介紹",
|
|
90
|
+
})
|
|
91
|
+
# ZH interrogative patterns: if these appear anywhere, the sentence is a
|
|
92
|
+
# question. Separate from _READONLY_INTENTS_ZH because these are positional-
|
|
93
|
+
# independent (e.g. "player.gd 裡面有什麼?" is a question without starting
|
|
94
|
+
# with an interrogative keyword).
|
|
95
|
+
_ZH_INTERROGATIVE_PATTERNS = (
|
|
96
|
+
"什麼", "什么", "嗎?", "嗎?", "何時", "哪個", "哪裡", "哪里",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def should_run_planner(user_input: str) -> tuple[bool, str]:
|
|
101
|
+
"""Heuristic: should the planner run for this user input?
|
|
102
|
+
|
|
103
|
+
Returns (run, reason). When run=False the caller should skip the
|
|
104
|
+
planner pass and emit a planner_skipped event with the reason.
|
|
105
|
+
|
|
106
|
+
Rules (in priority order):
|
|
107
|
+
1. Empty / whitespace-only → skip (caller already guards, this is defence)
|
|
108
|
+
2. Multiple file paths (res:// or .gd, >= 2) → run (multi-file scope)
|
|
109
|
+
3. > 5 lines → run (complex requests probably need planning)
|
|
110
|
+
4. Read-only intent keyword (first-token EN or 繁中 anywhere) → skip
|
|
111
|
+
— checked BEFORE action verbs so "如何實作 X" (how to implement X)
|
|
112
|
+
stays a question even though it contains an action verb.
|
|
113
|
+
5. Explicit action verb (EN or ZH) → run
|
|
114
|
+
6. Default → run (safe direction, preserves v1.0.0 behavior on ambiguity)
|
|
115
|
+
"""
|
|
116
|
+
text = user_input.strip()
|
|
117
|
+
if not text:
|
|
118
|
+
return False, "empty_input"
|
|
119
|
+
|
|
120
|
+
# Rule 2: multiple file paths — strong signal for multi-file change
|
|
121
|
+
file_refs = text.count("res://") + sum(1 for token in text.split() if token.endswith(".gd"))
|
|
122
|
+
if file_refs >= 2:
|
|
123
|
+
return True, f"multi_file:{file_refs}"
|
|
124
|
+
|
|
125
|
+
# Rule 3: complex / long requests
|
|
126
|
+
line_count = text.count("\n") + 1
|
|
127
|
+
if line_count > 5:
|
|
128
|
+
return True, f"long_request:{line_count}_lines"
|
|
129
|
+
|
|
130
|
+
# Rule 4: read-only intents — checked before action verbs so that
|
|
131
|
+
# question-framed inputs ("how do I X", "如何 X") are correctly
|
|
132
|
+
# classified as questions even when they contain action verbs.
|
|
133
|
+
lower = text.lower()
|
|
134
|
+
first_token = lower.split()[0] if lower.split() else ""
|
|
135
|
+
if first_token in _READONLY_INTENTS_EN:
|
|
136
|
+
return False, f"readonly_intent:{first_token}"
|
|
137
|
+
# ZH uses prefix matching (not substring) to avoid false positives
|
|
138
|
+
# like "改 enemy 的血量顯示" matching "顯示" as readonly.
|
|
139
|
+
for intent in _READONLY_INTENTS_ZH:
|
|
140
|
+
if text.startswith(intent):
|
|
141
|
+
return False, f"readonly_intent:{intent}"
|
|
142
|
+
# Positional-independent ZH interrogative patterns catch cases like
|
|
143
|
+
# "player.gd 裡面有什麼?" that don't start with a keyword.
|
|
144
|
+
for pattern in _ZH_INTERROGATIVE_PATTERNS:
|
|
145
|
+
if pattern in text:
|
|
146
|
+
return False, f"interrogative:{pattern}"
|
|
147
|
+
|
|
148
|
+
# Rule 5: explicit action verbs
|
|
149
|
+
words = set(lower.split())
|
|
150
|
+
for verb in _ACTION_VERBS_EN:
|
|
151
|
+
if verb in words:
|
|
152
|
+
return True, f"action_verb:{verb}"
|
|
153
|
+
# ZH action verbs use prefix matching for the same reason as readonly.
|
|
154
|
+
for verb in _ACTION_VERBS_ZH:
|
|
155
|
+
if text.startswith(verb):
|
|
156
|
+
return True, f"action_verb:{verb}"
|
|
157
|
+
|
|
158
|
+
# Rule 6: default — run. Preserves v1.0.0 behavior on ambiguous input.
|
|
159
|
+
return True, "default_run"
|
|
160
|
+
|
|
161
|
+
|
|
41
162
|
_FILE_MUTATING_TOOLS = {
|
|
42
163
|
"write_file",
|
|
43
164
|
"edit_file",
|
|
@@ -155,6 +276,21 @@ class ConversationEngine:
|
|
|
155
276
|
self.use_streaming = False
|
|
156
277
|
self.base_allowed_tools: set[str] | None = None
|
|
157
278
|
self.allowed_tools: set[str] | None = None
|
|
279
|
+
# v1.0.1/T1: how many planner blocks to keep in history. Default 2
|
|
280
|
+
# keeps the current plan plus one previous for "what did I just do"
|
|
281
|
+
# context without carrying N>2 worth of stale plans. Override via
|
|
282
|
+
# config (runtime/config.py `plan_history_keep`) or by setting on
|
|
283
|
+
# the engine instance directly.
|
|
284
|
+
self.plan_history_keep: int = 2
|
|
285
|
+
# v1.0.1/T2: skip the planner pass on trivial read-only turns.
|
|
286
|
+
# Set to False to restore v1.0.0's unconditional planner behavior.
|
|
287
|
+
self.planner_lazy: bool = True
|
|
288
|
+
# v1.0.1/D2: subprocess registry. Tools that spawn subprocesses
|
|
289
|
+
# (run_godot, screenshot_scene) register them here so Ctrl+C can
|
|
290
|
+
# terminate the entire subprocess tree, not just the Python
|
|
291
|
+
# coroutine. Populated via register_subprocess / cleared on
|
|
292
|
+
# rollback_and_terminate or after tool completion.
|
|
293
|
+
self._active_subprocesses: set[Any] = set()
|
|
158
294
|
self.active_skills: list[str] = []
|
|
159
295
|
self.skill_mode: str = "auto"
|
|
160
296
|
self.enabled_skills: list[str] = []
|
|
@@ -881,17 +1017,61 @@ class ConversationEngine:
|
|
|
881
1017
|
if self.dispatcher is None or self.mode not in {"apply", "fix"} or not _has_meaningful_text(user_input):
|
|
882
1018
|
return
|
|
883
1019
|
|
|
1020
|
+
# v1.0.1/T2: lazy planner — skip trivial read-only turns so we don't
|
|
1021
|
+
# pay the full planner cost (LLM call + impact report + plan
|
|
1022
|
+
# injection + rebill) on inputs that gain nothing from a plan pass.
|
|
1023
|
+
if self.planner_lazy:
|
|
1024
|
+
should_plan, reason = should_run_planner(user_input)
|
|
1025
|
+
if not should_plan:
|
|
1026
|
+
self._emit_event(
|
|
1027
|
+
"planner_skipped",
|
|
1028
|
+
f"Planner skipped: {reason}",
|
|
1029
|
+
reason=reason,
|
|
1030
|
+
)
|
|
1031
|
+
return
|
|
1032
|
+
|
|
884
1033
|
self._emit_event("planner_started", "Running planner pass")
|
|
885
1034
|
if self.project_path:
|
|
886
1035
|
self.last_impact_report = analyze_change_impact(Path(self.project_path), set(self.changeset.read_files) or {str(Path(self.project_path) / "project.godot")})
|
|
887
1036
|
planner_result = await self.dispatcher.run_planner(user_input)
|
|
888
1037
|
self.last_plan = planner_result.content
|
|
1038
|
+
# v1.0.1/T3: inject only the actionable subset (Goal + Steps) into
|
|
1039
|
+
# main history. The full plan with Risks/Validation is already
|
|
1040
|
+
# streamed to the user via the planner sub-engine's TUI output;
|
|
1041
|
+
# injecting it again here just wastes tokens on every rebill.
|
|
1042
|
+
from godot_agent.agents.dispatcher import extract_worker_plan
|
|
1043
|
+
reduced_plan = extract_worker_plan(planner_result.content)
|
|
889
1044
|
self.messages.append(
|
|
890
1045
|
Message.user(
|
|
891
|
-
f"[SYSTEM] Planner pass before implementation:\n{
|
|
1046
|
+
f"[SYSTEM] Planner pass before implementation:\n{reduced_plan}\n"
|
|
892
1047
|
"Follow this plan unless direct inspection or validation proves it wrong."
|
|
893
1048
|
)
|
|
894
1049
|
)
|
|
1050
|
+
# v1.0.1/T1+T4: prune old planner blocks from history so long sessions
|
|
1051
|
+
# don't accumulate N × 500-token plans that rebill on every subsequent
|
|
1052
|
+
# LLM call. Scoped by prefix so quality-gate / reviewer reports (also
|
|
1053
|
+
# [SYSTEM]-prefixed) are never touched as collateral damage.
|
|
1054
|
+
before_count = sum(
|
|
1055
|
+
1 for m in self.messages
|
|
1056
|
+
if isinstance(m.content, str) and m.content.startswith("[SYSTEM] Planner pass")
|
|
1057
|
+
)
|
|
1058
|
+
self.messages = prune_system_reports(
|
|
1059
|
+
self.messages,
|
|
1060
|
+
max_reports=self.plan_history_keep,
|
|
1061
|
+
prefix_filter="[SYSTEM] Planner pass",
|
|
1062
|
+
)
|
|
1063
|
+
after_count = sum(
|
|
1064
|
+
1 for m in self.messages
|
|
1065
|
+
if isinstance(m.content, str) and m.content.startswith("[SYSTEM] Planner pass")
|
|
1066
|
+
)
|
|
1067
|
+
dropped = before_count - after_count
|
|
1068
|
+
if dropped > 0:
|
|
1069
|
+
self._emit_event(
|
|
1070
|
+
"plan_pruned",
|
|
1071
|
+
f"Pruned {dropped} stale planner block(s) from history",
|
|
1072
|
+
dropped=dropped,
|
|
1073
|
+
kept=after_count,
|
|
1074
|
+
)
|
|
895
1075
|
self._emit_event("planner_finished", "Planner pass complete", used_tools=planner_result.used_tools)
|
|
896
1076
|
|
|
897
1077
|
async def submit(self, user_input: str) -> str:
|
|
@@ -903,10 +1083,18 @@ class ConversationEngine:
|
|
|
903
1083
|
self.last_user_input = user_input
|
|
904
1084
|
self.refresh_intent_profile(user_input)
|
|
905
1085
|
self._sync_registry_context()
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1086
|
+
# v1.0.1/D2: activate this engine as the subprocess registry so
|
|
1087
|
+
# tools spawned during this turn can register their subprocesses
|
|
1088
|
+
# for cancellation. The contextvar is reset in finally so
|
|
1089
|
+
# subsequent engines/tests get a clean state.
|
|
1090
|
+
registry_token = self._activate_subprocess_registry()
|
|
1091
|
+
try:
|
|
1092
|
+
await self._maybe_run_planner(user_input)
|
|
1093
|
+
self.messages.append(Message.user(user_input))
|
|
1094
|
+
self._emit_event("turn_started", user_input.splitlines()[0][:120], user_input=user_input)
|
|
1095
|
+
return await self._run_loop(None, use_streaming=self.use_streaming)
|
|
1096
|
+
finally:
|
|
1097
|
+
self._deactivate_subprocess_registry(registry_token)
|
|
910
1098
|
|
|
911
1099
|
def rollback_current_turn(self) -> int:
|
|
912
1100
|
"""Drop any messages appended since the last submit() began.
|
|
@@ -928,16 +1116,77 @@ class ConversationEngine:
|
|
|
928
1116
|
self._emit_event("turn_cancelled", f"turn cancelled, dropped {removed} messages")
|
|
929
1117
|
return removed
|
|
930
1118
|
|
|
1119
|
+
# ──────────────────────────────────────────────────────────────────
|
|
1120
|
+
# v1.0.1/D2: subprocess registry API
|
|
1121
|
+
# ──────────────────────────────────────────────────────────────────
|
|
1122
|
+
def register_subprocess(self, proc: Any) -> None:
|
|
1123
|
+
"""Register a subprocess so the engine can terminate it on
|
|
1124
|
+
cancellation. Tools call this after ``create_subprocess_exec``
|
|
1125
|
+
and ``unregister_subprocess`` after ``communicate`` completes.
|
|
1126
|
+
"""
|
|
1127
|
+
self._active_subprocesses.add(proc)
|
|
1128
|
+
|
|
1129
|
+
def unregister_subprocess(self, proc: Any) -> None:
|
|
1130
|
+
"""Remove a subprocess from the active set. Idempotent —
|
|
1131
|
+
unregistering an already-removed process is a no-op."""
|
|
1132
|
+
self._active_subprocesses.discard(proc)
|
|
1133
|
+
|
|
1134
|
+
async def terminate_active_subprocesses(self, timeout: float = 2.0) -> int:
|
|
1135
|
+
"""Terminate every registered subprocess that's still running.
|
|
1136
|
+
|
|
1137
|
+
Sends SIGTERM (via ``proc.terminate()``), waits up to ``timeout``
|
|
1138
|
+
for graceful exit, then escalates to SIGKILL for any survivor.
|
|
1139
|
+
Clears the registry after. Returns the number of processes that
|
|
1140
|
+
were still running when this was called (i.e. the count that
|
|
1141
|
+
required termination).
|
|
1142
|
+
"""
|
|
1143
|
+
if not self._active_subprocesses:
|
|
1144
|
+
return 0
|
|
1145
|
+
running = [p for p in self._active_subprocesses if p.returncode is None]
|
|
1146
|
+
for proc in running:
|
|
1147
|
+
try:
|
|
1148
|
+
proc.terminate()
|
|
1149
|
+
except ProcessLookupError:
|
|
1150
|
+
pass # already gone
|
|
1151
|
+
for proc in running:
|
|
1152
|
+
if proc.returncode is not None:
|
|
1153
|
+
continue
|
|
1154
|
+
try:
|
|
1155
|
+
await asyncio.wait_for(proc.wait(), timeout=timeout)
|
|
1156
|
+
except asyncio.TimeoutError:
|
|
1157
|
+
try:
|
|
1158
|
+
proc.kill()
|
|
1159
|
+
await proc.wait()
|
|
1160
|
+
except ProcessLookupError:
|
|
1161
|
+
pass
|
|
1162
|
+
self._active_subprocesses.clear()
|
|
1163
|
+
return len(running)
|
|
1164
|
+
|
|
1165
|
+
def _activate_subprocess_registry(self):
|
|
1166
|
+
"""Activate this engine as the current subprocess registry for
|
|
1167
|
+
the duration of a submit() call. Returns a token that must be
|
|
1168
|
+
passed to _deactivate_subprocess_registry to restore the prior
|
|
1169
|
+
state (matching contextvars' set/reset API)."""
|
|
1170
|
+
return _current_subprocess_registry.set(self)
|
|
1171
|
+
|
|
1172
|
+
def _deactivate_subprocess_registry(self, token) -> None:
|
|
1173
|
+
_current_subprocess_registry.reset(token)
|
|
1174
|
+
|
|
931
1175
|
async def submit_with_images(self, text: str, images_b64: list[str]) -> str:
|
|
932
1176
|
if not images_b64 and not _has_meaningful_text(text):
|
|
933
1177
|
return ""
|
|
934
1178
|
self.last_user_input = text
|
|
935
1179
|
self.refresh_intent_profile(text)
|
|
936
1180
|
self._sync_registry_context()
|
|
937
|
-
|
|
938
|
-
self.
|
|
939
|
-
|
|
940
|
-
|
|
1181
|
+
# v1.0.1/D2: activate subprocess registry for this turn (see submit)
|
|
1182
|
+
registry_token = self._activate_subprocess_registry()
|
|
1183
|
+
try:
|
|
1184
|
+
await self._maybe_run_planner(text)
|
|
1185
|
+
self.messages.append(Message.user_with_images(text, images_b64))
|
|
1186
|
+
self._emit_event("turn_started", text.splitlines()[0][:120], user_input=text, images=len(images_b64))
|
|
1187
|
+
return await self._run_loop(None, use_streaming=self.use_streaming)
|
|
1188
|
+
finally:
|
|
1189
|
+
self._deactivate_subprocess_registry(registry_token)
|
|
941
1190
|
|
|
942
1191
|
def _check_auto_health(self) -> "ContextHealth":
|
|
943
1192
|
from godot_agent.runtime.context_health import ContextHealth
|