devcouncil 0.3.1 → 0.4.1
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.
- package/README.md +92 -33
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +56 -40
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +964 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +325 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/summary.py +21 -3
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
from typing import List, Dict, Any, Type, Optional, TypeVar
|
|
1
|
+
from typing import List, Dict, Any, Type, Optional, TypeVar, cast
|
|
2
2
|
import copy
|
|
3
3
|
import functools
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
7
9
|
import time
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
12
|
+
import httpx
|
|
10
13
|
from pydantic import BaseModel
|
|
11
|
-
from devcouncil.llm.provider import Provider, LLMResponse
|
|
14
|
+
from devcouncil.llm.provider import Provider, LLMResponse, ProviderRequestError
|
|
12
15
|
from devcouncil.llm.cache import LLMCache
|
|
13
16
|
from devcouncil.telemetry.tracker import TelemetryTracker
|
|
14
17
|
from devcouncil.telemetry.traces import TraceLogger
|
|
18
|
+
from devcouncil.telemetry.stages import log_step
|
|
15
19
|
|
|
16
20
|
logger = logging.getLogger(__name__)
|
|
17
21
|
StructuredModel = TypeVar("StructuredModel", bound=BaseModel)
|
|
@@ -33,6 +37,32 @@ class StructuredOutputError(RuntimeError):
|
|
|
33
37
|
self.model = model
|
|
34
38
|
|
|
35
39
|
|
|
40
|
+
def _provider_retry_delay(exc: Exception, attempt: int) -> float:
|
|
41
|
+
"""Seconds to wait before retrying a failed provider call."""
|
|
42
|
+
if isinstance(exc, ProviderRequestError):
|
|
43
|
+
if exc.retry_after_seconds is not None:
|
|
44
|
+
return float(min(120.0, max(1.0, exc.retry_after_seconds)))
|
|
45
|
+
if exc.status_code == 429:
|
|
46
|
+
# OpenRouter tiers often cap at ~20 RPM; back off generously.
|
|
47
|
+
return float(min(90.0, 15.0 * (2 ** attempt)))
|
|
48
|
+
return min(30.0, float(2 ** attempt))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rate_limit_retry_budget() -> int:
|
|
52
|
+
"""How many 429 responses to wait out per call (beyond the normal attempts).
|
|
53
|
+
|
|
54
|
+
Default 8: with the 429 backoff above that is ~7 minutes of patience —
|
|
55
|
+
enough to ride out an RPM-window burst, small enough that a hard quota
|
|
56
|
+
(daily cap) still fails the call in bounded time. Override with
|
|
57
|
+
DEVCOUNCIL_RATE_LIMIT_RETRIES; 0 disables the separate budget."""
|
|
58
|
+
raw = os.environ.get("DEVCOUNCIL_RATE_LIMIT_RETRIES", "")
|
|
59
|
+
try:
|
|
60
|
+
value = int(raw)
|
|
61
|
+
except ValueError:
|
|
62
|
+
return 8
|
|
63
|
+
return max(0, value)
|
|
64
|
+
|
|
65
|
+
|
|
36
66
|
class ModelRouter:
|
|
37
67
|
# Independent fresh attempts at producing valid structured output before
|
|
38
68
|
# giving up. Even capable models occasionally emit malformed JSON; a second
|
|
@@ -45,6 +75,7 @@ class ModelRouter:
|
|
|
45
75
|
provider: Provider,
|
|
46
76
|
role_config: Dict[str, Dict[str, Any]],
|
|
47
77
|
project_root: Path = Path("."),
|
|
78
|
+
semantic_adapter: Optional[Any] = None,
|
|
48
79
|
):
|
|
49
80
|
self.provider = provider
|
|
50
81
|
self.role_config = role_config
|
|
@@ -55,6 +86,15 @@ class ModelRouter:
|
|
|
55
86
|
# concurrent-write safe.
|
|
56
87
|
self._cache = LLMCache(self.project_root)
|
|
57
88
|
self._traces = TraceLogger(self.project_root)
|
|
89
|
+
# Optional semantic cache / routing / compression (config-driven, lazy).
|
|
90
|
+
if semantic_adapter is not None:
|
|
91
|
+
self._semantic = semantic_adapter
|
|
92
|
+
else:
|
|
93
|
+
from devcouncil.llm.semantic_bridge import load_semantic_adapter
|
|
94
|
+
|
|
95
|
+
self._semantic = load_semantic_adapter(project_root)
|
|
96
|
+
if self._semantic is not None:
|
|
97
|
+
self._semantic.warm_up()
|
|
58
98
|
# Lazily-built providers for roles that override ``models.provider`` with
|
|
59
99
|
# their own ``provider:`` (e.g. live_reviewer on Ollama while planners run
|
|
60
100
|
# on OpenRouter). Keyed by normalized provider name; the default provider
|
|
@@ -95,34 +135,29 @@ class ModelRouter:
|
|
|
95
135
|
)
|
|
96
136
|
return self._role_providers[normalized]
|
|
97
137
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
# Inline reasoning blocks emitted by thinking models when the serving stack does
|
|
139
|
+
# not split them into a separate channel (local runners with a mismatched chat
|
|
140
|
+
# template are the common case). Removed before JSON extraction: the reasoning
|
|
141
|
+
# prose routinely contains JSON-looking examples that would otherwise be picked
|
|
142
|
+
# up instead of the real answer that follows the block.
|
|
143
|
+
_THINK_BLOCK_RE = re.compile(
|
|
144
|
+
r"<(think|thinking|reasoning|thought)>.*?</\1>", re.DOTALL | re.IGNORECASE
|
|
145
|
+
)
|
|
146
|
+
# An UNCLOSED reasoning tag (generation cut off or template quirk): everything
|
|
147
|
+
# from the opener is reasoning; nothing after it to salvage, but text BEFORE a
|
|
148
|
+
# dangling opener (rare) may hold the answer.
|
|
149
|
+
_THINK_OPEN_RE = re.compile(r"<(think|thinking|reasoning|thought)>", re.IGNORECASE)
|
|
101
150
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
— clean/fenced JSON is returned unchanged."""
|
|
109
|
-
text = content.strip()
|
|
110
|
-
if "```json" in text:
|
|
111
|
-
text = text.split("```json", 1)[1].split("```", 1)[0].strip()
|
|
112
|
-
elif "```" in text:
|
|
113
|
-
text = text.split("```", 1)[1].split("```", 1)[0].strip()
|
|
114
|
-
try:
|
|
115
|
-
json.loads(text)
|
|
116
|
-
return text
|
|
117
|
-
except Exception:
|
|
118
|
-
pass
|
|
119
|
-
for opener, closer in (("{", "}"), ("[", "]")):
|
|
120
|
-
start = text.find(opener)
|
|
121
|
-
if start == -1:
|
|
122
|
-
continue
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _balanced_candidates(text: str, opener: str, closer: str):
|
|
153
|
+
"""Yield every balanced top-level ``opener...closer`` span in ``text``,
|
|
154
|
+
string- and escape-aware so braces inside string values don't confuse it."""
|
|
155
|
+
start = text.find(opener)
|
|
156
|
+
while start != -1:
|
|
123
157
|
depth = 0
|
|
124
158
|
in_str = False
|
|
125
159
|
escaped = False
|
|
160
|
+
end = -1
|
|
126
161
|
for i in range(start, len(text)):
|
|
127
162
|
ch = text[i]
|
|
128
163
|
if in_str:
|
|
@@ -140,12 +175,56 @@ class ModelRouter:
|
|
|
140
175
|
elif ch == closer:
|
|
141
176
|
depth -= 1
|
|
142
177
|
if depth == 0:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
end = i
|
|
179
|
+
break
|
|
180
|
+
if end == -1:
|
|
181
|
+
return
|
|
182
|
+
yield text[start:end + 1]
|
|
183
|
+
start = text.find(opener, end + 1)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def _extract_json(cls, content: str) -> str:
|
|
187
|
+
"""Best-effort extraction of a JSON document from a model response.
|
|
188
|
+
|
|
189
|
+
Handles the common ways a model wraps valid JSON: inline ``<think>`` blocks
|
|
190
|
+
(local/thinking models whose serving stack leaves reasoning in the content),
|
|
191
|
+
triple-backtick fences, and surrounding prose ("Here you go: {...} thanks").
|
|
192
|
+
Strips reasoning blocks and fences, returns the whole thing if it already
|
|
193
|
+
parses, otherwise scans EVERY balanced object/array candidate (string- and
|
|
194
|
+
escape-aware) and returns the first that parses — a JSON-looking fragment in
|
|
195
|
+
leading prose no longer masks the real answer that follows it. Falls back to
|
|
196
|
+
the de-fenced text so the existing healing path still produces a meaningful
|
|
197
|
+
error. A strict superset of plain fence-stripping — clean/fenced JSON is
|
|
198
|
+
returned unchanged."""
|
|
199
|
+
text = content.strip()
|
|
200
|
+
# Drop closed reasoning blocks; on a dangling opener keep only what precedes it.
|
|
201
|
+
if "<" in text:
|
|
202
|
+
stripped = cls._THINK_BLOCK_RE.sub("", text).strip()
|
|
203
|
+
dangling = cls._THINK_OPEN_RE.search(stripped)
|
|
204
|
+
if dangling and "</" not in stripped[dangling.start():]:
|
|
205
|
+
before = stripped[:dangling.start()].strip()
|
|
206
|
+
after = stripped[dangling.end():].strip()
|
|
207
|
+
# The answer usually follows the (cut-off) reasoning; prefer whichever
|
|
208
|
+
# side actually contains a JSON-ish payload.
|
|
209
|
+
stripped = after if ("{" in after or "[" in after) else before
|
|
210
|
+
if stripped:
|
|
211
|
+
text = stripped
|
|
212
|
+
if "```json" in text:
|
|
213
|
+
text = text.split("```json", 1)[1].split("```", 1)[0].strip()
|
|
214
|
+
elif "```" in text:
|
|
215
|
+
text = text.split("```", 1)[1].split("```", 1)[0].strip()
|
|
216
|
+
try:
|
|
217
|
+
json.loads(text)
|
|
218
|
+
return text
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.debug("Response is not directly parseable JSON, scanning balanced candidates: %s", e)
|
|
221
|
+
for opener, closer in (("{", "}"), ("[", "]")):
|
|
222
|
+
for candidate in cls._balanced_candidates(text, opener, closer):
|
|
223
|
+
try:
|
|
224
|
+
json.loads(candidate)
|
|
225
|
+
return cast(str, candidate)
|
|
226
|
+
except Exception:
|
|
227
|
+
continue
|
|
149
228
|
return text
|
|
150
229
|
|
|
151
230
|
@staticmethod
|
|
@@ -173,15 +252,44 @@ class ModelRouter:
|
|
|
173
252
|
temperature: float,
|
|
174
253
|
run_id: Optional[str],
|
|
175
254
|
provider: Optional[Provider] = None,
|
|
176
|
-
attempts: int =
|
|
255
|
+
attempts: int = 5,
|
|
256
|
+
json_schema: Optional[Dict[str, Any]] = None,
|
|
177
257
|
) -> "LLMResponse":
|
|
178
258
|
"""Provider completion with bounded exponential-backoff retry. Used for BOTH the
|
|
179
259
|
initial call and the healing call so a transient fault in either is retried (and,
|
|
180
260
|
if still failing, surfaced to the caller's fallback logic) rather than aborting
|
|
181
261
|
the run. ``provider`` defaults to the router's default provider but may be a
|
|
182
|
-
per-role provider for roles that override ``models.provider``.
|
|
262
|
+
per-role provider for roles that override ``models.provider``. ``json_schema``
|
|
263
|
+
flows through to providers that support grammar-constrained structured output
|
|
264
|
+
(Ollama); others ignore it."""
|
|
265
|
+
if run_id is None:
|
|
266
|
+
# Fall back to the orchestrator-declared run so model_calls.jsonl
|
|
267
|
+
# records stay attributable even when call sites don't thread run_id.
|
|
268
|
+
from devcouncil.telemetry.context import get_current_run_id
|
|
269
|
+
|
|
270
|
+
run_id = get_current_run_id()
|
|
183
271
|
provider = provider or self.provider
|
|
184
|
-
|
|
272
|
+
# Only pass json_schema to providers whose ``complete`` accepts it — duck-typed
|
|
273
|
+
# or third-party Provider implementations may predate the parameter, and a
|
|
274
|
+
# structured-output OPTIMIZATION must never break them.
|
|
275
|
+
extra_kwargs: Dict[str, Any] = {}
|
|
276
|
+
if json_schema is not None:
|
|
277
|
+
import inspect
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
if "json_schema" in inspect.signature(provider.complete).parameters:
|
|
281
|
+
extra_kwargs["json_schema"] = json_schema
|
|
282
|
+
except (TypeError, ValueError):
|
|
283
|
+
pass
|
|
284
|
+
# Rate limiting (429) gets its OWN, larger budget: it is the provider
|
|
285
|
+
# telling us exactly when to come back (Retry-After), not a fault in the
|
|
286
|
+
# request — counting it against the shared ``attempts`` let a busy
|
|
287
|
+
# endpoint exhaust the budget and kill a run that only needed patience
|
|
288
|
+
# (observed: benchmark tasks dying blocked on limit_rpm mid-run).
|
|
289
|
+
max_rate_limit_retries = _rate_limit_retry_budget()
|
|
290
|
+
attempt = 0
|
|
291
|
+
rate_limit_retries = 0
|
|
292
|
+
while True:
|
|
185
293
|
try:
|
|
186
294
|
return await provider.complete(
|
|
187
295
|
model=model,
|
|
@@ -189,16 +297,57 @@ class ModelRouter:
|
|
|
189
297
|
temperature=temperature,
|
|
190
298
|
json_mode=True,
|
|
191
299
|
run_id=run_id,
|
|
300
|
+
**extra_kwargs,
|
|
301
|
+
)
|
|
302
|
+
except httpx.TimeoutException as exc:
|
|
303
|
+
# A timeout already consumed the provider's ENTIRE request window
|
|
304
|
+
# (600s by default on a local Ollama host). Retrying the identical
|
|
305
|
+
# request almost always times out again — and the structured-output
|
|
306
|
+
# layers above this (healing call + fresh attempts) would multiply
|
|
307
|
+
# the stall until the whole run is killed from outside (observed:
|
|
308
|
+
# benchmark arms burning their full 20-minute budget on 2x 600s
|
|
309
|
+
# timeouts and dying with exit 124 before producing a verdict).
|
|
310
|
+
# Fail fast with an actionable message instead.
|
|
311
|
+
raise ProviderRequestError(
|
|
312
|
+
f"LLM request to model '{model}' timed out after the provider's "
|
|
313
|
+
f"request window ({exc!r}). If this is a local (Ollama) model, "
|
|
314
|
+
"generation is too slow for the configured window: raise "
|
|
315
|
+
"OLLAMA_TIMEOUT, cap generation with OLLAMA_NUM_PREDICT, reduce "
|
|
316
|
+
"thinking with OLLAMA_THINK=low or OLLAMA_THINK=false, or use a "
|
|
317
|
+
"smaller/faster model."
|
|
318
|
+
) from exc
|
|
319
|
+
except ProviderRequestError as exc:
|
|
320
|
+
if exc.status_code == 429 and rate_limit_retries < max_rate_limit_retries:
|
|
321
|
+
rate_limit_retries += 1
|
|
322
|
+
delay = _provider_retry_delay(exc, rate_limit_retries)
|
|
323
|
+
logger.warning(
|
|
324
|
+
"LLM provider rate-limited (429, retry %d/%d): %r. Retrying in %.0fs...",
|
|
325
|
+
rate_limit_retries, max_rate_limit_retries, exc, delay,
|
|
326
|
+
)
|
|
327
|
+
await asyncio.sleep(delay)
|
|
328
|
+
continue
|
|
329
|
+
attempt += 1
|
|
330
|
+
if attempt >= attempts:
|
|
331
|
+
raise
|
|
332
|
+
delay = _provider_retry_delay(exc, attempt - 1)
|
|
333
|
+
logger.warning(
|
|
334
|
+
"LLM provider request failed (attempt %d/%d): %r. Retrying in %.0fs...",
|
|
335
|
+
attempt, attempts, exc, delay,
|
|
192
336
|
)
|
|
337
|
+
await asyncio.sleep(delay)
|
|
193
338
|
except Exception as exc:
|
|
194
|
-
|
|
339
|
+
attempt += 1
|
|
340
|
+
if attempt >= attempts:
|
|
195
341
|
raise
|
|
342
|
+
delay = _provider_retry_delay(exc, attempt - 1)
|
|
343
|
+
# %r, not %s: common failures (httpx.ReadTimeout, CancelledError)
|
|
344
|
+
# stringify to an EMPTY message, which previously logged the useless
|
|
345
|
+
# "failed (attempt 1/3): ." and made timeouts undiagnosable from logs.
|
|
196
346
|
logger.warning(
|
|
197
|
-
"LLM request failed (attempt %d/%d): %
|
|
198
|
-
attempt
|
|
347
|
+
"LLM request failed (attempt %d/%d): %r. Retrying in %.0fs...",
|
|
348
|
+
attempt, attempts, exc, delay,
|
|
199
349
|
)
|
|
200
|
-
await asyncio.sleep(
|
|
201
|
-
raise RuntimeError("unreachable") # loop either returns or raises
|
|
350
|
+
await asyncio.sleep(delay)
|
|
202
351
|
|
|
203
352
|
async def complete_structured(
|
|
204
353
|
self,
|
|
@@ -217,9 +366,22 @@ class ModelRouter:
|
|
|
217
366
|
model = config["model"]
|
|
218
367
|
temp = temperature if temperature is not None else config.get("temperature", 0.0)
|
|
219
368
|
provider = self._provider_for_role(config)
|
|
220
|
-
|
|
369
|
+
role_provider = config.get("provider")
|
|
370
|
+
|
|
221
371
|
# Deep-copy to avoid mutating the caller's messages list
|
|
222
372
|
msgs = copy.deepcopy(messages)
|
|
373
|
+
|
|
374
|
+
# Optional long-context compression before schema injection.
|
|
375
|
+
if self._semantic is not None:
|
|
376
|
+
msgs = await self._semantic.maybe_compress_messages_async(msgs)
|
|
377
|
+
|
|
378
|
+
# Optional complexity-based model routing (local Ollama only when enabled).
|
|
379
|
+
if self._semantic is not None:
|
|
380
|
+
model = await self._semantic.maybe_route_model_async(
|
|
381
|
+
msgs,
|
|
382
|
+
configured_model=model,
|
|
383
|
+
role_provider=role_provider,
|
|
384
|
+
)
|
|
223
385
|
|
|
224
386
|
# Add schema instructions to system or user message. Spell out "instance, not
|
|
225
387
|
# the schema" explicitly: weaker/local models otherwise sometimes echo the schema
|
|
@@ -259,12 +421,46 @@ class ModelRouter:
|
|
|
259
421
|
# Check cache first
|
|
260
422
|
response = cache.get(model, msgs, temp, True, provider_fp)
|
|
261
423
|
cache_hit = response is not None
|
|
424
|
+
semantic_cache_hit = False
|
|
425
|
+
|
|
426
|
+
if not cache_hit and self._semantic is not None:
|
|
427
|
+
response = await self._semantic.lookup_cache_async(msgs, model=model, role=role)
|
|
428
|
+
if response is not None:
|
|
429
|
+
cache_hit = True
|
|
430
|
+
semantic_cache_hit = True
|
|
431
|
+
|
|
432
|
+
# Structured-output schema for providers with grammar-constrained decoding
|
|
433
|
+
# (Ollama's native ``format: <schema>``): the model cannot emit invalid JSON,
|
|
434
|
+
# which on weak/local models eliminates most schema echoes and healing rounds.
|
|
435
|
+
structured_schema = schema.model_json_schema()
|
|
262
436
|
|
|
263
437
|
started = time.monotonic()
|
|
264
438
|
if not response:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
439
|
+
try:
|
|
440
|
+
response = await self._complete_with_retry(
|
|
441
|
+
model=model, messages=msgs, temperature=temp, run_id=run_id, provider=provider,
|
|
442
|
+
json_schema=structured_schema,
|
|
443
|
+
)
|
|
444
|
+
except ProviderRequestError as exc:
|
|
445
|
+
# A degradable role (caller supplied a fallback) must degrade on a
|
|
446
|
+
# provider failure exactly as it does on unparseable output: the
|
|
447
|
+
# fallback exists to keep the run alive on a flaky/slow model, and a
|
|
448
|
+
# fail-fast timeout (see _complete_with_retry) or exhausted retries
|
|
449
|
+
# is the same class of "this role produced nothing usable".
|
|
450
|
+
if fallback is not None:
|
|
451
|
+
logger.warning(
|
|
452
|
+
"Role '%s' (model '%s') provider request failed (%s); "
|
|
453
|
+
"using a safe fallback so the run can continue.",
|
|
454
|
+
role, model, exc,
|
|
455
|
+
)
|
|
456
|
+
traces.log_event(
|
|
457
|
+
"llm_provider_request_failed_fallback",
|
|
458
|
+
{"role": role, "model": model, "error": str(exc)},
|
|
459
|
+
run_id=run_id,
|
|
460
|
+
summary=f"Provider request failed for {role}; degraded to fallback.",
|
|
461
|
+
)
|
|
462
|
+
return fallback
|
|
463
|
+
raise
|
|
268
464
|
elapsed = time.monotonic() - started
|
|
269
465
|
|
|
270
466
|
if response is None:
|
|
@@ -273,12 +469,26 @@ class ModelRouter:
|
|
|
273
469
|
if not cache_hit:
|
|
274
470
|
tracker.log_usage(model, response.usage, local=provider_local)
|
|
275
471
|
|
|
472
|
+
cache_label = "cache_hit" if cache_hit else f"{elapsed:.1f}s"
|
|
473
|
+
if semantic_cache_hit:
|
|
474
|
+
cache_label = "semantic_cache_hit"
|
|
475
|
+
|
|
276
476
|
# Include latency + cache status: on a slow (e.g. local) model this is what tells
|
|
277
477
|
# you *which* call dominated a multi-minute planning/verification stage.
|
|
278
478
|
logger.info(
|
|
279
479
|
"LLM response: role=%s model=%s tokens=%s %s",
|
|
280
480
|
role, response.model, response.usage,
|
|
281
|
-
|
|
481
|
+
cache_label,
|
|
482
|
+
)
|
|
483
|
+
log_step(
|
|
484
|
+
f"llm/{role}: {response.model} {cache_label}",
|
|
485
|
+
project_root=self.project_root,
|
|
486
|
+
run_id=run_id,
|
|
487
|
+
role=role,
|
|
488
|
+
model=response.model,
|
|
489
|
+
latency_s=round(elapsed, 2) if not cache_hit else 0,
|
|
490
|
+
cache_hit=cache_hit,
|
|
491
|
+
semantic_cache_hit=semantic_cache_hit,
|
|
282
492
|
)
|
|
283
493
|
|
|
284
494
|
try:
|
|
@@ -288,6 +498,8 @@ class ModelRouter:
|
|
|
288
498
|
result = schema.model_validate(data)
|
|
289
499
|
if not cache_hit:
|
|
290
500
|
cache.set(model, msgs, temp, True, response, provider_fp) # cache only validated output
|
|
501
|
+
if self._semantic is not None and not semantic_cache_hit:
|
|
502
|
+
await self._semantic.store_cache_async(msgs, response, model=model, role=role)
|
|
291
503
|
return result
|
|
292
504
|
except Exception as e:
|
|
293
505
|
logger.warning(f"Initial parse failed for {role}, attempting healing: {e}")
|
|
@@ -323,6 +535,16 @@ Content:
|
|
|
323
535
|
{echo_hint}
|
|
324
536
|
Please return the corrected JSON object only. No prose.
|
|
325
537
|
"""
|
|
538
|
+
# The healing call must SEE the schema. Previously it got only the error
|
|
539
|
+
# + bad content, so a "Field required" failure asked the model to invent
|
|
540
|
+
# the missing fields blind — on providers without grammar-constrained
|
|
541
|
+
# decoding (OpenRouter/Vertex, e.g. gemini-2.5-flash omitting empty
|
|
542
|
+
# list fields) healing then failed the same way and the whole planning
|
|
543
|
+
# run crashed. Reuse the same schema instruction as the initial call.
|
|
544
|
+
healing_messages = [
|
|
545
|
+
{"role": "system", "content": f"You repair malformed JSON.{instruction}"},
|
|
546
|
+
{"role": "user", "content": healing_prompt},
|
|
547
|
+
]
|
|
326
548
|
# The healing completion runs INSIDE this try (with the same retry/backoff as
|
|
327
549
|
# the initial call). A transient failure here (429/timeout) must be treated as
|
|
328
550
|
# "healing failed" so it routes into the fresh-attempt/fallback logic below,
|
|
@@ -332,16 +554,19 @@ Please return the corrected JSON object only. No prose.
|
|
|
332
554
|
# We use a lower temperature for healing
|
|
333
555
|
healed_response = await self._complete_with_retry(
|
|
334
556
|
model=model,
|
|
335
|
-
messages=
|
|
557
|
+
messages=healing_messages,
|
|
336
558
|
temperature=0.0,
|
|
337
559
|
run_id=run_id,
|
|
338
560
|
provider=provider,
|
|
561
|
+
json_schema=structured_schema,
|
|
339
562
|
)
|
|
340
563
|
tracker.log_usage(healed_response.model, healed_response.usage, local=provider_local)
|
|
341
564
|
healed_content = self._extract_json(healed_response.content)
|
|
342
565
|
data = json.loads(healed_content)
|
|
343
566
|
result = schema.model_validate(data)
|
|
344
567
|
cache.set(model, msgs, temp, True, healed_response, provider_fp)
|
|
568
|
+
if self._semantic is not None:
|
|
569
|
+
await self._semantic.store_cache_async(msgs, healed_response, model=model, role=role)
|
|
345
570
|
return result
|
|
346
571
|
except Exception as final_e:
|
|
347
572
|
logger.error(f"Healing failed for {role}: {final_e}")
|