gdmcode 0.1.8__tar.gz → 0.1.10__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.
- {gdmcode-0.1.8 → gdmcode-0.1.10}/PKG-INFO +1 -1
- gdmcode-0.1.10/gdmcode/__init__.py +1 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/loop.py +52 -28
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/cli.py +12 -3
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/mcp_server.py +1 -1
- gdmcode-0.1.10/gdmcode/intent_router.py +223 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/repl.py +15 -58
- {gdmcode-0.1.8 → gdmcode-0.1.10}/pyproject.toml +1 -1
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_cli_smoke.py +22 -0
- gdmcode-0.1.10/tests/test_intent_router.py +51 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_loop.py +4 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_repl_smoke.py +22 -1
- gdmcode-0.1.8/gdmcode/__init__.py +0 -1
- {gdmcode-0.1.8 → gdmcode-0.1.10}/.gitignore +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/CONTRIBUTING.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/README.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/config.toml +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/agentic-runtime-audit.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/architecture.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/cli-reference.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/configuration.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/deployment.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/plugin-guide.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/quick-start.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/docs/security-hardening.md +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/_internal/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/_internal/constants.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/_internal/domain_skills.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/commit_classifier.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/context_budget.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/daemon.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/dag_validator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/debug_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/impact_analyzer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/impact_graph.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/orchestrator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/regression_guard.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/review_gate.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/risk_scorer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/self_healing.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/smart_test_selector.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/system_prompt.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/task_tracker.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/test_validator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/tool_orchestrator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/transcript.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/verification_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/work_director.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/agent/worktree_manager.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/artifacts/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/artifacts/artifact_store.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/artifacts/verification_graph.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/auth.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/commands.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/config.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/cost_tracker.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/db/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/db/migrations.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/audit_log.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/identity.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/rbac.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/team_config.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/enterprise/usage_analytics.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/exceptions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/git_workflow.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/github_actions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/sentry_integration.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/sentry_server.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/integrations/webhook_security.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/main.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/code_index.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/compressor.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/context_memory.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/continuous_memory.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/conventions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/db.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/document_index.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/file_cache.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/project_scanner.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/memory/session_store.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/models/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/models/client.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/models/definitions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/models/router.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/models/schemas.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/permissions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/command_filter.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/models.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/permission_handler.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/phone_ui.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/protocol.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/qr.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/server.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/token_manager.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/remote/tunnel.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/runtime/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/runtime/branch_farm.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/runtime/replay.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sandbox/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sandbox/hermetic.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sandbox/policy.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sdk/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sdk/plugin_base.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sdk/plugin_host.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/sdk/plugin_loader.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/security.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/server/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/server/bridge.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/server/bridge_cli.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/server/bridge_client.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/server/protocol_version.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/session/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/session/event_fanout.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/session/input_broker.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/session/permission_bridge.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/_atomic.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/agent_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/ask_user_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/bash_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/browser_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/browser_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/dep_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/document_reader.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/document_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/document_writer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/impact_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/playwright_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/quality_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/read_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/result_cache.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/search_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/shell_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/tools/write_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/audio_capture.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/audio_playback.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/errors.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/models.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/providers.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/vad.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/gdmcode/voice/voice_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/proxy/Dockerfile +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/proxy/main.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/proxy/requirements.txt +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/remote/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/remote/test_remote_server.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_agent_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_agent_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_api_fallback.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_artifact_store.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_audit_log.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_auth.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_auto_quality.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_autonomy_levels.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_bash_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_batch_api.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_branch_farm.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_bridge.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_bridge_smoke.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_browser_tool_smoke.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_browser_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_btw_queue.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_budget_tracker.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_chrome_extension.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_ci_runner.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_code_index.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_commands.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_compression.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_confidence.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_config.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_continuous_memory.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_convention_drift.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_cost_tracker.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_daemon.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_daemon_stability.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_daemon_watchdog.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_db.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_debate.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_debug_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_debug_loop_smoke.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_dep_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_doctor.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_document_index.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_document_reader.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_document_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_document_writer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_domain_skills.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_eval_harness.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_event_log.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_failure_taxonomy.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_file_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_git_workflow.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_github_actions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_health.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_hermetic_sandbox.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_identity_rbac.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_impact_analysis.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_impact_analyzer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_impact_graph.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_impact_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_injection_gate.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_leaderboard.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_local_models.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_loop_p3.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_mcp_server.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_memory.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_migrations.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_mock_provider.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_model_config.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_orchestrator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_package.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_permissions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_phase2_modules.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_phone_ui.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_playwright_tool.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_plugin_sdk.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_protocol_version.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_provenance.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_proxy_server.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_quality_integration.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_reasoning_toggle.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_redaction.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_regression_collector.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_regression_guard_integration.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_regression_runner.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_replay.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_resilience.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_result_cache.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_review_gate_expanded.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_risk_scorer.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_rollback.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_router.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_router_compressor_conventions.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_router_escalation.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_sandbox.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_scoring.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_search_tools.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_self_healing.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_semantic_edit.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_sentry_integration.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_session_checkpoint.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_session_controller.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_session_restore.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_signal_handling.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_swebench_adapter.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_swebench_runner.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_system_prompt.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_team_config.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_tool_cache.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_tool_orchestrator.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_tool_timeout.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_tools_registry.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_tunnel_qr.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_usage_analytics.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_verification_graph.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_verification_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_voice_loop.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_voice_providers.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_whole_codebase.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/test_work_director.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/voice/__init__.py +0 -0
- {gdmcode-0.1.8 → gdmcode-0.1.10}/tests/voice/test_audio_foundation.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.10"
|
|
@@ -50,33 +50,33 @@ try:
|
|
|
50
50
|
_analytics: "_UsageAnalytics | None" = _UsageAnalytics()
|
|
51
51
|
except Exception: # noqa: BLE001
|
|
52
52
|
_analytics = None
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
from gdmcode.agent.risk_scorer import score_patch, RiskTier
|
|
56
|
-
_risk_scorer_available = True
|
|
57
|
-
except Exception: # noqa: BLE001
|
|
58
|
-
_risk_scorer_available = False
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _check_patch_risk(diff, file_paths=None, autonomy_level=2):
|
|
62
|
-
"""Score a patch diff and enforce risk gates based on autonomy level.
|
|
63
|
-
|
|
64
|
-
- CRITICAL (score >= block_threshold) and autonomy_level < 4: raises RuntimeError.
|
|
65
|
-
- HIGH tier and autonomy_level < 3: logs a warning (non-blocking).
|
|
66
|
-
- Always returns the PatchRiskResult (or None if scorer unavailable).
|
|
67
|
-
"""
|
|
68
|
-
if not _risk_scorer_available:
|
|
69
|
-
return None
|
|
70
|
-
result = score_patch(diff, file_paths)
|
|
71
|
-
log.info("patch_risk score=%.3f tier=%s blocked=%s", result.score, result.tier, result.blocked)
|
|
72
|
-
if result.triggered_signals:
|
|
73
|
-
log.debug("patch_risk rationale:\n%s", result.rationale)
|
|
74
|
-
if result.blocked and autonomy_level < 4:
|
|
75
|
-
raise RuntimeError(f"Patch blocked: {result.rationale}")
|
|
76
|
-
if result.tier == RiskTier.HIGH and autonomy_level < 3:
|
|
77
|
-
log.warning("High-risk patch detected (score=%.3f). Review before applying:\n%s",
|
|
78
|
-
result.score, result.rationale)
|
|
79
|
-
return result
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from gdmcode.agent.risk_scorer import score_patch, RiskTier
|
|
56
|
+
_risk_scorer_available = True
|
|
57
|
+
except Exception: # noqa: BLE001
|
|
58
|
+
_risk_scorer_available = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _check_patch_risk(diff, file_paths=None, autonomy_level=2):
|
|
62
|
+
"""Score a patch diff and enforce risk gates based on autonomy level.
|
|
63
|
+
|
|
64
|
+
- CRITICAL (score >= block_threshold) and autonomy_level < 4: raises RuntimeError.
|
|
65
|
+
- HIGH tier and autonomy_level < 3: logs a warning (non-blocking).
|
|
66
|
+
- Always returns the PatchRiskResult (or None if scorer unavailable).
|
|
67
|
+
"""
|
|
68
|
+
if not _risk_scorer_available:
|
|
69
|
+
return None
|
|
70
|
+
result = score_patch(diff, file_paths)
|
|
71
|
+
log.info("patch_risk score=%.3f tier=%s blocked=%s", result.score, result.tier, result.blocked)
|
|
72
|
+
if result.triggered_signals:
|
|
73
|
+
log.debug("patch_risk rationale:\n%s", result.rationale)
|
|
74
|
+
if result.blocked and autonomy_level < 4:
|
|
75
|
+
raise RuntimeError(f"Patch blocked: {result.rationale}")
|
|
76
|
+
if result.tier == RiskTier.HIGH and autonomy_level < 3:
|
|
77
|
+
log.warning("High-risk patch detected (score=%.3f). Review before applying:\n%s",
|
|
78
|
+
result.score, result.rationale)
|
|
79
|
+
return result
|
|
80
80
|
|
|
81
81
|
def _record_usage(
|
|
82
82
|
session_id: str,
|
|
@@ -505,6 +505,11 @@ class AgentLoop:
|
|
|
505
505
|
self._model = get_model(tier, self._cfg.provider)
|
|
506
506
|
self._model_id = self._model.id
|
|
507
507
|
self._gdm_client = self._make_client()
|
|
508
|
+
yield AgentEvent(
|
|
509
|
+
EventType.THINKING,
|
|
510
|
+
content=f"Using {self._model_id} for this turn.",
|
|
511
|
+
turn=0,
|
|
512
|
+
)
|
|
508
513
|
log.debug(
|
|
509
514
|
"ModelRouter selected tier=%s mode=%s for prompt=%r",
|
|
510
515
|
tier,
|
|
@@ -587,6 +592,14 @@ class AgentLoop:
|
|
|
587
592
|
log.warning("BTW queue drain failed: %s", _btw_exc)
|
|
588
593
|
|
|
589
594
|
tools = self._build_tool_specs()
|
|
595
|
+
yield AgentEvent(
|
|
596
|
+
EventType.THINKING,
|
|
597
|
+
content=(
|
|
598
|
+
"Asking the model to choose the next step"
|
|
599
|
+
+ (f" with {len(tools)} available tools." if tools else ".")
|
|
600
|
+
),
|
|
601
|
+
turn=turn_num,
|
|
602
|
+
)
|
|
590
603
|
try:
|
|
591
604
|
response = self._gdm_client.complete(
|
|
592
605
|
self._transcript.to_messages(),
|
|
@@ -686,6 +699,13 @@ class AgentLoop:
|
|
|
686
699
|
|
|
687
700
|
# Execute tool calls
|
|
688
701
|
if raw_tool_calls:
|
|
702
|
+
tool_names = ", ".join(str(tc.get("function", {}).get("name", "tool")) for tc in raw_tool_calls[:3])
|
|
703
|
+
extra = "" if len(raw_tool_calls) <= 3 else f" (+{len(raw_tool_calls) - 3} more)"
|
|
704
|
+
yield AgentEvent(
|
|
705
|
+
EventType.THINKING,
|
|
706
|
+
content=f"Model selected tool step: {tool_names}{extra}.",
|
|
707
|
+
turn=turn_num,
|
|
708
|
+
)
|
|
689
709
|
# Guard: finish_reason=tool_calls with empty list → infinite loop
|
|
690
710
|
if not msg.tool_calls:
|
|
691
711
|
yield AgentEvent(EventType.ERROR, turn=turn_num,
|
|
@@ -694,6 +714,11 @@ class AgentLoop:
|
|
|
694
714
|
return
|
|
695
715
|
|
|
696
716
|
yield from self._execute_tool_calls(msg.tool_calls, turn_num)
|
|
717
|
+
yield AgentEvent(
|
|
718
|
+
EventType.THINKING,
|
|
719
|
+
content="Feeding tool results back to the model.",
|
|
720
|
+
turn=turn_num,
|
|
721
|
+
)
|
|
697
722
|
continue # loop back to model with tool results
|
|
698
723
|
|
|
699
724
|
# Natural completion
|
|
@@ -1407,4 +1432,3 @@ def write_autonomy_audit(
|
|
|
1407
1432
|
(session_id, time.time(), level, action, json.dumps(details), checkpoint_id),
|
|
1408
1433
|
)
|
|
1409
1434
|
db_conn.commit()
|
|
1410
|
-
|
|
@@ -29,6 +29,7 @@ import shutil
|
|
|
29
29
|
import httpx
|
|
30
30
|
import typer
|
|
31
31
|
from rich.console import Console
|
|
32
|
+
from rich.markup import escape
|
|
32
33
|
|
|
33
34
|
from gdmcode import __version__
|
|
34
35
|
from gdmcode.config import load_config
|
|
@@ -1231,14 +1232,15 @@ def _run_one_shot(cfg: object, prompt: str, *, yes: bool, model_override: str |
|
|
|
1231
1232
|
"""
|
|
1232
1233
|
from rich.status import Status
|
|
1233
1234
|
from gdmcode.agent.loop import EventType
|
|
1234
|
-
from gdmcode.
|
|
1235
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
1236
|
+
from gdmcode.repl import _format_llm_error
|
|
1235
1237
|
|
|
1236
1238
|
if not _has_model_connection(cfg):
|
|
1237
1239
|
console.print("[red]No model connection configured.[/red]")
|
|
1238
1240
|
console.print("Run [bold]gdm login grok[/bold], [bold]gdm login gemini[/bold], or start [bold]gdm[/bold] and use [bold]/proxy token[/bold] then [bold]/proxy on[/bold].")
|
|
1239
1241
|
raise typer.Exit(1)
|
|
1240
1242
|
|
|
1241
|
-
if
|
|
1243
|
+
if classify_prompt_route(prompt, cfg, model_override=model_override) == PromptRoute.LIGHTWEIGHT:
|
|
1242
1244
|
_run_connection_probe_once(cfg, prompt, model_override=model_override)
|
|
1243
1245
|
return
|
|
1244
1246
|
|
|
@@ -1300,7 +1302,14 @@ def _run_one_shot(cfg: object, prompt: str, *, yes: bool, model_override: str |
|
|
|
1300
1302
|
had_error = False
|
|
1301
1303
|
try:
|
|
1302
1304
|
for event in loop.run(prompt): # type: ignore[union-attr]
|
|
1303
|
-
if event.type == EventType.
|
|
1305
|
+
if event.type == EventType.THINKING:
|
|
1306
|
+
status.stop()
|
|
1307
|
+
snippet = str(event.content or "")[:120]
|
|
1308
|
+
if snippet:
|
|
1309
|
+
console.print(f"[dim cyan]… {snippet}[/dim cyan]")
|
|
1310
|
+
status.start()
|
|
1311
|
+
status.update("[cyan]Working...[/cyan]")
|
|
1312
|
+
elif event.type == EventType.RESPONSE:
|
|
1304
1313
|
status.stop()
|
|
1305
1314
|
console.print(event.content)
|
|
1306
1315
|
elif event.type == EventType.ERROR:
|
|
@@ -51,7 +51,7 @@ _INTERNAL_ERROR = -32603
|
|
|
51
51
|
class MCPServer:
|
|
52
52
|
"""Minimal MCP server with stdio transport."""
|
|
53
53
|
|
|
54
|
-
def __init__(self, name: str = "gdmcode", version: str = "0.1.
|
|
54
|
+
def __init__(self, name: str = "gdmcode", version: str = "0.1.10") -> None:
|
|
55
55
|
self._name = name
|
|
56
56
|
self._version = version
|
|
57
57
|
self._tools: dict[str, MCPTool] = {}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Prompt intent routing for lightweight chat vs full agent mode."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"PromptRoute",
|
|
12
|
+
"classify_prompt_route",
|
|
13
|
+
"is_lightweight_prompt_heuristic",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PromptRoute(str, Enum):
|
|
18
|
+
"""Possible execution routes for a user prompt."""
|
|
19
|
+
|
|
20
|
+
LIGHTWEIGHT = "LIGHTWEIGHT"
|
|
21
|
+
AGENT = "AGENT"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_WORK_TERMS: frozenset[str] = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"fix",
|
|
27
|
+
"edit",
|
|
28
|
+
"change",
|
|
29
|
+
"create",
|
|
30
|
+
"write",
|
|
31
|
+
"delete",
|
|
32
|
+
"read",
|
|
33
|
+
"open",
|
|
34
|
+
"search",
|
|
35
|
+
"find",
|
|
36
|
+
"run",
|
|
37
|
+
"test",
|
|
38
|
+
"build",
|
|
39
|
+
"install",
|
|
40
|
+
"commit",
|
|
41
|
+
"review",
|
|
42
|
+
"debug",
|
|
43
|
+
"explain",
|
|
44
|
+
"refactor",
|
|
45
|
+
"implement",
|
|
46
|
+
"update",
|
|
47
|
+
"file",
|
|
48
|
+
"folder",
|
|
49
|
+
"repo",
|
|
50
|
+
"code",
|
|
51
|
+
"command",
|
|
52
|
+
"shell",
|
|
53
|
+
"terminal",
|
|
54
|
+
"powershell",
|
|
55
|
+
"bash",
|
|
56
|
+
"internet",
|
|
57
|
+
"web",
|
|
58
|
+
"download",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_LIGHTWEIGHT_EXACT: frozenset[str] = frozenset(
|
|
63
|
+
{
|
|
64
|
+
"hey",
|
|
65
|
+
"hi",
|
|
66
|
+
"hello",
|
|
67
|
+
"yo",
|
|
68
|
+
"ping",
|
|
69
|
+
"hello there",
|
|
70
|
+
"hey there",
|
|
71
|
+
"hi there",
|
|
72
|
+
"are you there",
|
|
73
|
+
"you there",
|
|
74
|
+
"who are you",
|
|
75
|
+
"what are you",
|
|
76
|
+
"what can you do",
|
|
77
|
+
"are you working",
|
|
78
|
+
"are you connected",
|
|
79
|
+
"is the llm connected",
|
|
80
|
+
"status",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_LIGHTWEIGHT_PREFIXES: tuple[str, ...] = (
|
|
85
|
+
"who are you",
|
|
86
|
+
"what are you",
|
|
87
|
+
"what can you do",
|
|
88
|
+
"are you there",
|
|
89
|
+
"are you connected",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_CLASSIFIER_SYSTEM = """Classify the user's prompt for a CLI coding agent.
|
|
93
|
+
|
|
94
|
+
Reply with exactly one token:
|
|
95
|
+
LIGHTWEIGHT - greetings, identity/capability questions, connection/status checks, or normal chat that does not need files, repo context, tools, internet, or commands.
|
|
96
|
+
AGENT - anything that asks to inspect, read, search, edit, create, delete, run, test, build, install, debug, explain code/files/repo, use tools, access internet, or continue previous work.
|
|
97
|
+
|
|
98
|
+
When uncertain, reply AGENT.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalize(text: str) -> str:
|
|
103
|
+
normalized = re.sub(r"[^a-z0-9\s']", " ", text.lower()).strip()
|
|
104
|
+
return re.sub(r"\s+", " ", normalized)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _looks_like_agent_work(text: str, normalized: str, words: list[str]) -> bool:
|
|
108
|
+
if any(word in _WORK_TERMS for word in words):
|
|
109
|
+
return True
|
|
110
|
+
if len(words) > 16:
|
|
111
|
+
return True
|
|
112
|
+
if "```" in text or "`" in text:
|
|
113
|
+
return True
|
|
114
|
+
if re.search(r"(?:^|\s)(?:\.?[/\\]|[a-zA-Z]:\\)", text):
|
|
115
|
+
return True
|
|
116
|
+
if re.search(r"\b[\w.-]+\.(?:py|js|ts|tsx|jsx|json|toml|yaml|yml|md|txt|go|rs|java|cs|cpp|c|h|html|css)\b", text, re.I):
|
|
117
|
+
return True
|
|
118
|
+
if normalized.startswith(("can you ", "please ", "could you ")) and any(word in _WORK_TERMS for word in normalized.split()):
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_lightweight_prompt_heuristic(text: str) -> bool:
|
|
124
|
+
"""Return True only for prompts that are clearly lightweight without LLM help."""
|
|
125
|
+
normalized = _normalize(text)
|
|
126
|
+
if not normalized:
|
|
127
|
+
return False
|
|
128
|
+
words = normalized.split()
|
|
129
|
+
if _looks_like_agent_work(text, normalized, words):
|
|
130
|
+
return False
|
|
131
|
+
if normalized in _LIGHTWEIGHT_EXACT:
|
|
132
|
+
return True
|
|
133
|
+
if len(words) <= 8 and normalized.startswith(_LIGHTWEIGHT_PREFIXES):
|
|
134
|
+
return True
|
|
135
|
+
return len(words) <= 4 and words[0] in {"hey", "hi", "hello"} and all(
|
|
136
|
+
word in {"hey", "hi", "hello", "there", "gdm", "agent", "please"} for word in words
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _has_model_route(cfg: object) -> bool:
|
|
141
|
+
return bool(
|
|
142
|
+
getattr(cfg, "api_key", "")
|
|
143
|
+
or (
|
|
144
|
+
getattr(cfg, "proxy_enabled", False)
|
|
145
|
+
and getattr(cfg, "proxy_url", "")
|
|
146
|
+
and getattr(cfg, "proxy_token", "")
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _classifier_client(cfg: object):
|
|
152
|
+
from gdmcode.models.client import GdmClient
|
|
153
|
+
|
|
154
|
+
proxy_active = bool(
|
|
155
|
+
getattr(cfg, "proxy_enabled", False)
|
|
156
|
+
and getattr(cfg, "proxy_url", "")
|
|
157
|
+
and getattr(cfg, "proxy_token", "")
|
|
158
|
+
)
|
|
159
|
+
return (
|
|
160
|
+
GdmClient.for_proxy(getattr(cfg, "proxy_url"), getattr(cfg, "proxy_token"))
|
|
161
|
+
if proxy_active
|
|
162
|
+
else GdmClient(cfg) # type: ignore[arg-type]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _parse_route(raw: str) -> PromptRoute:
|
|
167
|
+
return PromptRoute.LIGHTWEIGHT if raw.strip().upper() == PromptRoute.LIGHTWEIGHT else PromptRoute.AGENT
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def classify_prompt_route(
|
|
171
|
+
prompt: str,
|
|
172
|
+
cfg: object,
|
|
173
|
+
*,
|
|
174
|
+
model_override: str | None = None,
|
|
175
|
+
allow_llm: bool = True,
|
|
176
|
+
) -> PromptRoute:
|
|
177
|
+
"""Classify a prompt into lightweight chat or full agent mode.
|
|
178
|
+
|
|
179
|
+
The router is intentionally conservative: obvious work never calls the
|
|
180
|
+
classifier, unparseable classifier output becomes AGENT, and classifier
|
|
181
|
+
failures fall back only to the strict allow-list heuristic.
|
|
182
|
+
"""
|
|
183
|
+
normalized = _normalize(prompt)
|
|
184
|
+
if not normalized:
|
|
185
|
+
return PromptRoute.AGENT
|
|
186
|
+
|
|
187
|
+
words = normalized.split()
|
|
188
|
+
if _looks_like_agent_work(prompt, normalized, words):
|
|
189
|
+
log.debug("prompt_route route=AGENT source=heuristic_work prompt=%r", prompt[:80])
|
|
190
|
+
return PromptRoute.AGENT
|
|
191
|
+
|
|
192
|
+
if is_lightweight_prompt_heuristic(prompt):
|
|
193
|
+
log.debug("prompt_route route=LIGHTWEIGHT source=heuristic_light prompt=%r", prompt[:80])
|
|
194
|
+
return PromptRoute.LIGHTWEIGHT
|
|
195
|
+
|
|
196
|
+
if not allow_llm or not _has_model_route(cfg):
|
|
197
|
+
route = PromptRoute.LIGHTWEIGHT if is_lightweight_prompt_heuristic(prompt) else PromptRoute.AGENT
|
|
198
|
+
log.debug("prompt_route route=%s source=fallback_no_llm prompt=%r", route.value, prompt[:80])
|
|
199
|
+
return route
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from gdmcode.models.definitions import ModelTier, get_model
|
|
203
|
+
|
|
204
|
+
tier = model_override or ModelTier.SCOUT
|
|
205
|
+
model_def = get_model(tier, getattr(cfg, "provider"))
|
|
206
|
+
raw = _classifier_client(cfg).complete_text(
|
|
207
|
+
prompt,
|
|
208
|
+
model=model_def.id,
|
|
209
|
+
max_tokens=4,
|
|
210
|
+
system=_CLASSIFIER_SYSTEM,
|
|
211
|
+
)
|
|
212
|
+
route = _parse_route(raw)
|
|
213
|
+
log.debug(
|
|
214
|
+
"prompt_route route=%s source=llm raw=%r prompt=%r",
|
|
215
|
+
route.value,
|
|
216
|
+
raw[:80],
|
|
217
|
+
prompt[:80],
|
|
218
|
+
)
|
|
219
|
+
return route
|
|
220
|
+
except Exception as exc: # noqa: BLE001
|
|
221
|
+
fallback = PromptRoute.LIGHTWEIGHT if is_lightweight_prompt_heuristic(prompt) else PromptRoute.AGENT
|
|
222
|
+
log.warning("Prompt intent classifier failed; using %s fallback: %s", fallback.value, exc)
|
|
223
|
+
return fallback
|
|
@@ -163,59 +163,10 @@ def _format_llm_error(error: object) -> str:
|
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
def _is_connection_probe(text: str) -> bool:
|
|
166
|
-
"""
|
|
167
|
-
import
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
normalized = re.sub(r"\s+", " ", normalized)
|
|
171
|
-
if not normalized:
|
|
172
|
-
return False
|
|
173
|
-
work_terms = {
|
|
174
|
-
"fix", "edit", "change", "create", "write", "delete", "read", "open", "search",
|
|
175
|
-
"find", "run", "test", "build", "install", "commit", "review", "debug", "explain",
|
|
176
|
-
"refactor", "implement", "update", "file", "folder", "repo", "code",
|
|
177
|
-
}
|
|
178
|
-
words = normalized.split()
|
|
179
|
-
if any(word in work_terms for word in words):
|
|
180
|
-
return False
|
|
181
|
-
direct = {
|
|
182
|
-
"hey",
|
|
183
|
-
"hi",
|
|
184
|
-
"hello",
|
|
185
|
-
"yo",
|
|
186
|
-
"ping",
|
|
187
|
-
"test",
|
|
188
|
-
"hello there",
|
|
189
|
-
"hey there",
|
|
190
|
-
"hi there",
|
|
191
|
-
"are you there",
|
|
192
|
-
"you there",
|
|
193
|
-
"who are you",
|
|
194
|
-
"what are you",
|
|
195
|
-
"what can you do",
|
|
196
|
-
"are you working",
|
|
197
|
-
"are you connected",
|
|
198
|
-
"is the llm connected",
|
|
199
|
-
"are tools working",
|
|
200
|
-
"do tools work",
|
|
201
|
-
"status",
|
|
202
|
-
}
|
|
203
|
-
if normalized in direct:
|
|
204
|
-
return True
|
|
205
|
-
prefixes = (
|
|
206
|
-
"who are you",
|
|
207
|
-
"what are you",
|
|
208
|
-
"what can you do",
|
|
209
|
-
"are you there",
|
|
210
|
-
"are you connected",
|
|
211
|
-
"are tools working",
|
|
212
|
-
"do tools work",
|
|
213
|
-
)
|
|
214
|
-
if len(words) <= 8 and normalized.startswith(prefixes):
|
|
215
|
-
return True
|
|
216
|
-
return len(words) <= 4 and words[0] in {"hey", "hi", "hello"} and all(
|
|
217
|
-
word in {"hey", "hi", "hello", "there", "gdm", "agent", "please"} for word in words
|
|
218
|
-
)
|
|
166
|
+
"""Compatibility wrapper for tests and older imports."""
|
|
167
|
+
from gdmcode.intent_router import is_lightweight_prompt_heuristic
|
|
168
|
+
|
|
169
|
+
return is_lightweight_prompt_heuristic(text)
|
|
219
170
|
|
|
220
171
|
|
|
221
172
|
def _render_event(event: object, status: Status, console: Console) -> bool:
|
|
@@ -226,8 +177,11 @@ def _render_event(event: object, status: Status, console: Console) -> bool:
|
|
|
226
177
|
match ev.type: # type: ignore[union-attr]
|
|
227
178
|
case EventType.THINKING:
|
|
228
179
|
snippet = str(ev.content or "")[:100] # type: ignore[union-attr]
|
|
229
|
-
|
|
230
|
-
|
|
180
|
+
status.stop()
|
|
181
|
+
if snippet:
|
|
182
|
+
console.print(f"[dim cyan]… {escape(snippet)}[/dim cyan]")
|
|
183
|
+
status.start()
|
|
184
|
+
status.update("[cyan]Working...[/cyan]")
|
|
231
185
|
case EventType.TOOL_CALL:
|
|
232
186
|
status.stop()
|
|
233
187
|
console.print(f"[yellow][tool] {ev.tool_name}({_fmt_args(ev.args)})[/yellow]") # type: ignore[union-attr]
|
|
@@ -690,9 +644,12 @@ def start_repl(cfg: "GdmConfig", db: "GdmDatabase", *, yes: bool = False, model_
|
|
|
690
644
|
console.print("[yellow]Restore not supported by current agent.[/yellow]")
|
|
691
645
|
continue
|
|
692
646
|
|
|
693
|
-
if
|
|
694
|
-
|
|
695
|
-
|
|
647
|
+
if loop is None:
|
|
648
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
649
|
+
|
|
650
|
+
if classify_prompt_route(text, cfg, model_override=model_override) == PromptRoute.LIGHTWEIGHT:
|
|
651
|
+
_run_connection_probe(text)
|
|
652
|
+
continue
|
|
696
653
|
|
|
697
654
|
pending = _show_pending_btw(db, session_id, console)
|
|
698
655
|
full_message = text
|
|
@@ -132,3 +132,25 @@ class TestCliSmoke:
|
|
|
132
132
|
assert result.exit_code == 0
|
|
133
133
|
probe.assert_called_once()
|
|
134
134
|
assert probe.call_args.args[1] == "who are you?"
|
|
135
|
+
|
|
136
|
+
def test_one_shot_ambiguous_prompt_uses_llm_router(self, tmp_path, monkeypatch) -> None:
|
|
137
|
+
import gdmcode.cli as cli
|
|
138
|
+
import gdmcode.intent_router as intent_router
|
|
139
|
+
|
|
140
|
+
cfg = SimpleNamespace(provider="gemini", api_key="key", project_root=tmp_path)
|
|
141
|
+
monkeypatch.setattr(cli, "load_config", lambda require_credentials=False: cfg)
|
|
142
|
+
monkeypatch.setattr(cli, "_print_header", lambda _cfg: None)
|
|
143
|
+
monkeypatch.setattr(cli, "_has_model_connection", lambda _cfg: True)
|
|
144
|
+
monkeypatch.setattr(
|
|
145
|
+
intent_router,
|
|
146
|
+
"classify_prompt_route",
|
|
147
|
+
lambda *_args, **_kwargs: intent_router.PromptRoute.LIGHTWEIGHT,
|
|
148
|
+
)
|
|
149
|
+
probe = MagicMock()
|
|
150
|
+
monkeypatch.setattr(cli, "_run_connection_probe_once", probe)
|
|
151
|
+
|
|
152
|
+
result = runner.invoke(app, ["code", "--prompt", "what's your name?"])
|
|
153
|
+
|
|
154
|
+
assert result.exit_code == 0
|
|
155
|
+
probe.assert_called_once()
|
|
156
|
+
assert probe.call_args.args[1] == "what's your name?"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _cfg() -> SimpleNamespace:
|
|
8
|
+
return SimpleNamespace(
|
|
9
|
+
provider="gemini",
|
|
10
|
+
api_key="key",
|
|
11
|
+
proxy_enabled=False,
|
|
12
|
+
proxy_url="",
|
|
13
|
+
proxy_token="",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_obvious_work_routes_to_agent_without_llm() -> None:
|
|
18
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
19
|
+
|
|
20
|
+
with patch("gdmcode.models.client.GdmClient.complete_text") as complete_text:
|
|
21
|
+
route = classify_prompt_route("can you inspect the repo and run tests?", _cfg())
|
|
22
|
+
|
|
23
|
+
assert route == PromptRoute.AGENT
|
|
24
|
+
complete_text.assert_not_called()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_ambiguous_prompt_can_be_classified_lightweight_by_llm() -> None:
|
|
28
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
29
|
+
|
|
30
|
+
with patch("gdmcode.models.client.GdmClient.complete_text", return_value="LIGHTWEIGHT"):
|
|
31
|
+
route = classify_prompt_route("what's your name?", _cfg())
|
|
32
|
+
|
|
33
|
+
assert route == PromptRoute.LIGHTWEIGHT
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_unparseable_classifier_output_defaults_to_agent() -> None:
|
|
37
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
38
|
+
|
|
39
|
+
with patch("gdmcode.models.client.GdmClient.complete_text", return_value="This seems lightweight."):
|
|
40
|
+
route = classify_prompt_route("what's your name?", _cfg())
|
|
41
|
+
|
|
42
|
+
assert route == PromptRoute.AGENT
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_classifier_error_falls_back_conservatively() -> None:
|
|
46
|
+
from gdmcode.intent_router import PromptRoute, classify_prompt_route
|
|
47
|
+
|
|
48
|
+
with patch("gdmcode.models.client.GdmClient.complete_text", side_effect=RuntimeError("boom")):
|
|
49
|
+
route = classify_prompt_route("what's your name?", _cfg())
|
|
50
|
+
|
|
51
|
+
assert route == PromptRoute.AGENT
|
|
@@ -268,6 +268,10 @@ class TestAgentLoop:
|
|
|
268
268
|
|
|
269
269
|
mock_orchestrator.execute.assert_called_once_with("bash", {"command": "ls"}, model_id=loop._model.id)
|
|
270
270
|
event_types = [e.type for e in events]
|
|
271
|
+
thinking = [e.content for e in events if e.type == EventType.THINKING]
|
|
272
|
+
assert any("Asking the model" in content for content in thinking)
|
|
273
|
+
assert any("Model selected tool step: bash" in content for content in thinking)
|
|
274
|
+
assert any("Feeding tool results back" in content for content in thinking)
|
|
271
275
|
assert EventType.TOOL_CALL in event_types
|
|
272
276
|
assert EventType.TOOL_RESULT in event_types
|
|
273
277
|
assert EventType.DONE in event_types
|
|
@@ -46,7 +46,7 @@ class TestReplHelpers:
|
|
|
46
46
|
assert _is_connection_probe("hello there")
|
|
47
47
|
assert _is_connection_probe("who are you?")
|
|
48
48
|
assert _is_connection_probe("what can you do?")
|
|
49
|
-
assert _is_connection_probe("are tools working?")
|
|
49
|
+
assert not _is_connection_probe("are tools working?")
|
|
50
50
|
assert not _is_connection_probe("fix the failing tests")
|
|
51
51
|
assert not _is_connection_probe("explain this file")
|
|
52
52
|
|
|
@@ -204,6 +204,27 @@ class TestReplStartRepl:
|
|
|
204
204
|
assert "coding-agent CLI" in output
|
|
205
205
|
run_agent.assert_not_called()
|
|
206
206
|
|
|
207
|
+
def test_ambiguous_lightweight_prompt_uses_llm_router(self, tmp_path, capsys) -> None:
|
|
208
|
+
from gdmcode.repl import start_repl
|
|
209
|
+
|
|
210
|
+
cfg, db = self._make_mocks(tmp_path)
|
|
211
|
+
cfg.api_key = "test-key"
|
|
212
|
+
|
|
213
|
+
with patch("gdmcode.repl._build_input_fn") as mock_build:
|
|
214
|
+
mock_build.return_value = MagicMock(side_effect=["what's your name?", EOFError])
|
|
215
|
+
with patch(
|
|
216
|
+
"gdmcode.models.client.GdmClient.complete_text",
|
|
217
|
+
side_effect=["LIGHTWEIGHT", "I am gdm, your coding-agent CLI."],
|
|
218
|
+
):
|
|
219
|
+
with patch("gdmcode.repl._run_agent_turn") as run_agent:
|
|
220
|
+
with patch("gdmcode.cost_tracker.CostTracker"):
|
|
221
|
+
with patch("gdmcode.repl._ensure_session", return_value="session-123"):
|
|
222
|
+
start_repl(cfg, db)
|
|
223
|
+
|
|
224
|
+
output = capsys.readouterr().out
|
|
225
|
+
assert "coding-agent CLI" in output
|
|
226
|
+
run_agent.assert_not_called()
|
|
227
|
+
|
|
207
228
|
|
|
208
229
|
class TestReplErrorFormatting:
|
|
209
230
|
def test_format_llm_error_rate_limit(self) -> None:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|