codenib 0.1.0__py3-none-any.whl
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.
- codenib/__init__.py +62 -0
- codenib/__main__.py +7 -0
- codenib/_lazy.py +36 -0
- codenib/_version.py +26 -0
- codenib/agent/__init__.py +159 -0
- codenib/agent/agent_types.py +40 -0
- codenib/agent/boundary.py +129 -0
- codenib/agent/compile.py +219 -0
- codenib/agent/extract_agent.py +101 -0
- codenib/agent/harness.py +326 -0
- codenib/agent/history.py +242 -0
- codenib/agent/lsp_graph.py +878 -0
- codenib/agent/lsp_provider.py +484 -0
- codenib/agent/rerank_agent.py +419 -0
- codenib/agent/resource_guard.py +90 -0
- codenib/agent/route_context.py +391 -0
- codenib/agent/runner.py +2005 -0
- codenib/agent/runtime/__init__.py +16 -0
- codenib/agent/runtime/context.py +189 -0
- codenib/agent/runtime/trace.py +101 -0
- codenib/agent/skills/__init__.py +25 -0
- codenib/agent/skills/_graphnav.py +235 -0
- codenib/agent/skills/bm25_search/__init__.py +0 -0
- codenib/agent/skills/bm25_search/config.yaml +63 -0
- codenib/agent/skills/bm25_search/executor.py +80 -0
- codenib/agent/skills/bm25_search/skill.md +47 -0
- codenib/agent/skills/code_to_query/__init__.py +0 -0
- codenib/agent/skills/code_to_query/config.yaml +38 -0
- codenib/agent/skills/code_to_query/executor.py +69 -0
- codenib/agent/skills/code_to_query/skill.md +7 -0
- codenib/agent/skills/codenib_context/__init__.py +0 -0
- codenib/agent/skills/codenib_context/config.yaml +44 -0
- codenib/agent/skills/codenib_context/executor.py +184 -0
- codenib/agent/skills/codenib_context/skill.md +21 -0
- codenib/agent/skills/context.py +107 -0
- codenib/agent/skills/core.py +153 -0
- codenib/agent/skills/crossencoder_rerank/__init__.py +3 -0
- codenib/agent/skills/crossencoder_rerank/config.yaml +41 -0
- codenib/agent/skills/crossencoder_rerank/executor.py +62 -0
- codenib/agent/skills/crossencoder_rerank/skill.md +7 -0
- codenib/agent/skills/embedding_search/__init__.py +0 -0
- codenib/agent/skills/embedding_search/config.yaml +53 -0
- codenib/agent/skills/embedding_search/executor.py +57 -0
- codenib/agent/skills/embedding_search/skill.md +47 -0
- codenib/agent/skills/find_callees/__init__.py +0 -0
- codenib/agent/skills/find_callees/config.yaml +22 -0
- codenib/agent/skills/find_callees/executor.py +23 -0
- codenib/agent/skills/find_callees/skill.md +11 -0
- codenib/agent/skills/find_callers/__init__.py +0 -0
- codenib/agent/skills/find_callers/config.yaml +22 -0
- codenib/agent/skills/find_callers/executor.py +23 -0
- codenib/agent/skills/find_callers/skill.md +11 -0
- codenib/agent/skills/hybrid_search/__init__.py +0 -0
- codenib/agent/skills/hybrid_search/config.yaml +38 -0
- codenib/agent/skills/hybrid_search/executor.py +132 -0
- codenib/agent/skills/hybrid_search/skill.md +53 -0
- codenib/agent/skills/llm_rerank/__init__.py +0 -0
- codenib/agent/skills/llm_rerank/config.yaml +33 -0
- codenib/agent/skills/llm_rerank/executor.py +37 -0
- codenib/agent/skills/llm_rerank/skill.md +7 -0
- codenib/agent/skills/loader.py +240 -0
- codenib/agent/skills/lsp_definition/config.yaml +35 -0
- codenib/agent/skills/lsp_definition/executor.py +32 -0
- codenib/agent/skills/lsp_definition/skill.md +18 -0
- codenib/agent/skills/lsp_references/config.yaml +40 -0
- codenib/agent/skills/lsp_references/executor.py +34 -0
- codenib/agent/skills/lsp_references/skill.md +17 -0
- codenib/agent/skills/lsp_route/config.yaml +35 -0
- codenib/agent/skills/lsp_route/executor.py +34 -0
- codenib/agent/skills/lsp_route/skill.md +23 -0
- codenib/agent/skills/registry.py +136 -0
- codenib/agent/skills/repository_search/config.yaml +49 -0
- codenib/agent/skills/repository_search/executor.py +512 -0
- codenib/agent/skills/repository_search/skill.md +28 -0
- codenib/agent/skills/trace/__init__.py +0 -0
- codenib/agent/skills/trace/config.yaml +30 -0
- codenib/agent/skills/trace/executor.py +27 -0
- codenib/agent/skills/trace/skill.md +12 -0
- codenib/agent/skills/typecheck.py +302 -0
- codenib/agent/tool_schema.py +158 -0
- codenib/agent/tools/__init__.py +41 -0
- codenib/agent/tools/defaults.py +1045 -0
- codenib/agent/tools/spec.py +103 -0
- codenib/agent/utils.py +149 -0
- codenib/cli.py +1079 -0
- codenib/clients/__init__.py +5 -0
- codenib/clients/claude_agent.py +534 -0
- codenib/clients/codex_agent.py +314 -0
- codenib/code_chunker.py +730 -0
- codenib/code_chunking/__init__.py +113 -0
- codenib/code_chunking/base.py +592 -0
- codenib/code_chunking/cpp_chunker.py +278 -0
- codenib/code_chunking/csharp_chunker.py +210 -0
- codenib/code_chunking/go_chunker.py +213 -0
- codenib/code_chunking/java_chunker.py +188 -0
- codenib/code_chunking/js_chunker.py +251 -0
- codenib/code_chunking/kotlin_chunker.py +253 -0
- codenib/code_chunking/lua_chunker.py +94 -0
- codenib/code_chunking/php_chunker.py +204 -0
- codenib/code_chunking/python_chunker.py +267 -0
- codenib/code_chunking/ruby_chunker.py +198 -0
- codenib/code_chunking/rust_chunker.py +195 -0
- codenib/code_chunking/scala_chunker.py +182 -0
- codenib/code_chunking/swift_chunker.py +182 -0
- codenib/compat_pickle.py +48 -0
- codenib/compiler/__init__.py +93 -0
- codenib/compiler/index_builders.py +868 -0
- codenib/compiler/index_compiler.py +488 -0
- codenib/compiler/manifest.py +221 -0
- codenib/compiler/params.py +126 -0
- codenib/compiler/resources.py +267 -0
- codenib/compiler/skill_context.py +641 -0
- codenib/compiler/snapshot_store.py +311 -0
- codenib/compiler/verification.py +131 -0
- codenib/dataset/__init__.py +21 -0
- codenib/dataset/base.py +78 -0
- codenib/dataset/codenib_base.py +374 -0
- codenib/dataset/codenib_synthesis.py +540 -0
- codenib/dataset/collect/difficulty_classifier.py +511 -0
- codenib/dataset/collect/swebench_sample.py +766 -0
- codenib/dataset/gt_locate.py +831 -0
- codenib/dataset/local_json.py +184 -0
- codenib/dataset/locbench.py +254 -0
- codenib/dataset/swebench.py +344 -0
- codenib/dataset/swebench_multilingual.py +283 -0
- codenib/dataset/synthesize/__init__.py +71 -0
- codenib/dataset/synthesize/_agent.py +125 -0
- codenib/dataset/synthesize/_types.py +146 -0
- codenib/dataset/synthesize/context_loader.py +601 -0
- codenib/dataset/synthesize/query_curator.py +713 -0
- codenib/dataset/synthesize/query_synthesizer.py +546 -0
- codenib/dataset/synthesize/verifier.py +425 -0
- codenib/dataset/synthesize/vocab_guard.py +264 -0
- codenib/dataset/utils.py +380 -0
- codenib/eval/agent_runner/__init__.py +296 -0
- codenib/eval/agent_runner/baseline.py +299 -0
- codenib/eval/agent_runner/batch.py +176 -0
- codenib/eval/agent_runner/contexts.py +135 -0
- codenib/eval/agent_runner/feedback.py +135 -0
- codenib/eval/agent_runner/feedback_summary.py +326 -0
- codenib/eval/agent_runner/format_diagnostics.py +115 -0
- codenib/eval/agent_runner/live_lsp_provider.py +377 -0
- codenib/eval/agent_runner/loc_baseline.py +355 -0
- codenib/eval/agent_runner/lsp_agent_ab.py +577 -0
- codenib/eval/agent_runner/lsp_agent_study.py +408 -0
- codenib/eval/agent_runner/lsp_agent_study_analysis.py +411 -0
- codenib/eval/agent_runner/lsp_agent_study_artifacts.py +413 -0
- codenib/eval/agent_runner/lsp_agent_study_manifest.py +402 -0
- codenib/eval/agent_runner/lsp_agent_study_runner.py +694 -0
- codenib/eval/agent_runner/lsp_baseline.py +251 -0
- codenib/eval/agent_runner/lsp_latency.py +267 -0
- codenib/eval/agent_runner/lsp_provider_cli.py +273 -0
- codenib/eval/agent_runner/lsp_provider_validation.py +594 -0
- codenib/eval/agent_runner/lsp_readiness.py +167 -0
- codenib/eval/agent_runner/lsp_replay_benchmark.py +1072 -0
- codenib/eval/agent_runner/metrics.py +80 -0
- codenib/eval/agent_runner/orchestrator.py +173 -0
- codenib/eval/agent_runner/pareto.py +154 -0
- codenib/eval/agent_runner/prebuilt.py +484 -0
- codenib/eval/agent_runner/preload.py +219 -0
- codenib/eval/agent_runner/promotion.py +172 -0
- codenib/eval/agent_runner/query_sweep.py +432 -0
- codenib/eval/agent_runner/results.py +69 -0
- codenib/eval/agent_runner/scoring.py +134 -0
- codenib/eval/agent_runner/sweep.py +619 -0
- codenib/eval/agent_runner/sweep_config.py +160 -0
- codenib/eval/agent_runner/symbols.py +67 -0
- codenib/eval/agent_runner/trace_summary.py +405 -0
- codenib/eval/agent_runner/verify_expand.py +219 -0
- codenib/eval/artifact_bundle.py +322 -0
- codenib/eval/artifact_integrity.py +332 -0
- codenib/eval/artifact_manifest.py +236 -0
- codenib/eval/experiments/__init__.py +5 -0
- codenib/eval/experiments/lsp_agent_study_policy.py +55 -0
- codenib/eval/loc_agent_runner.py +37 -0
- codenib/eval/reports/__init__.py +5 -0
- codenib/eval/reports/cost_arm_report.py +810 -0
- codenib/eval/retrieval_eval.py +557 -0
- codenib/graph/__init__.py +44 -0
- codenib/graph/backend_alignment.py +184 -0
- codenib/graph/code_graph.py +1218 -0
- codenib/graph/dependency.py +229 -0
- codenib/graph/hierarchy.py +828 -0
- codenib/graph/incremental/__init__.py +15 -0
- codenib/graph/incremental/change_mgr.py +165 -0
- codenib/graph/incremental/graph_patcher.py +139 -0
- codenib/graph/incremental/lsp_client.py +1271 -0
- codenib/graph/incremental/patcher_base.py +1364 -0
- codenib/graph/incremental/patcher_cpp.py +796 -0
- codenib/graph/incremental/patcher_go.py +56 -0
- codenib/graph/incremental/patcher_python.py +43 -0
- codenib/graph/incremental/patcher_rust.py +113 -0
- codenib/graph/incremental/patcher_ts.py +49 -0
- codenib/graph/incremental/subgraph_mgr.py +1028 -0
- codenib/graph/layers.py +325 -0
- codenib/graph/roi_subgraph.py +394 -0
- codenib/graph/setup.py +689 -0
- codenib/graph/traverse_graph.py +237 -0
- codenib/index/__init__.py +42 -0
- codenib/index/embedding/__init__.py +48 -0
- codenib/index/embedding/builders.py +190 -0
- codenib/index/embedding/model_policy.py +62 -0
- codenib/index/embedding/prompt_registry.py +87 -0
- codenib/index/embedding/vector_store.py +1458 -0
- codenib/index/incremental/__init__.py +30 -0
- codenib/index/incremental/chunk_store.py +409 -0
- codenib/index/incremental/embeddings_cache.py +196 -0
- codenib/index/incremental/git_diff.py +221 -0
- codenib/index/incremental/index_updater.py +283 -0
- codenib/index/incremental/state.py +92 -0
- codenib/index/regex_idx/__init__.py +7 -0
- codenib/index/regex_idx/regex_idx.py +150 -0
- codenib/index/rerank/__init__.py +23 -0
- codenib/index/rerank/cross_encoder.py +356 -0
- codenib/index/sparse_idx/__init__.py +9 -0
- codenib/index/sparse_idx/bm25_index.py +724 -0
- codenib/index/trigram/__init__.py +17 -0
- codenib/index/trigram/zoekt_searcher.py +371 -0
- codenib/languages.py +1035 -0
- codenib/llm/__init__.py +33 -0
- codenib/llm/diagnostics.py +170 -0
- codenib/llm/litellm_chat.py +422 -0
- codenib/llm/options.py +139 -0
- codenib/llm/usage.py +182 -0
- codenib/log_utils.py +301 -0
- codenib/ls_index/__init__.py +17 -0
- codenib/ls_index/clangd_decode.py +912 -0
- codenib/ls_index/clangd_indexer.py +1400 -0
- codenib/ls_index/index_quality.py +442 -0
- codenib/ls_index/lsp_graph_decode.py +449 -0
- codenib/ls_index/lsp_indexer.py +246 -0
- codenib/ls_router.py +507 -0
- codenib/mcp/__init__.py +13 -0
- codenib/mcp/__main__.py +9 -0
- codenib/mcp/context.py +211 -0
- codenib/mcp/prompts.py +76 -0
- codenib/mcp/server.py +463 -0
- codenib/mcp/tools/__init__.py +9 -0
- codenib/mcp/tools/dependency.py +56 -0
- codenib/mcp/tools/lsp.py +119 -0
- codenib/mcp/tools/search.py +224 -0
- codenib/model/__init__.py +49 -0
- codenib/model/agentless_pipeline.py +484 -0
- codenib/model/bm25_retrieve_pipeline.py +100 -0
- codenib/model/dense_graph_expand_rerank_pipeline.py +300 -0
- codenib/model/embedding_retrieve_pipeline.py +152 -0
- codenib/model/graph_augmented_rerank_pipeline.py +19 -0
- codenib/model/graph_retrieve_pipeline.py +259 -0
- codenib/model/hybrid_retrieve_pipeline.py +256 -0
- codenib/model/retrieval_planner.py +377 -0
- codenib/model/retrieve_rerank_pipeline.py +1023 -0
- codenib/ops/expand.py +299 -0
- codenib/ops/filter.py +159 -0
- codenib/ops/rerank.py +246 -0
- codenib/ops/retrieve.py +293 -0
- codenib/ops/transform.py +34 -0
- codenib/paths.py +91 -0
- codenib/profiler.py +506 -0
- codenib/repository_filters.py +103 -0
- codenib/repository_summary.py +161 -0
- codenib/scip_interface/__init__.py +120 -0
- codenib/scip_interface/lsp_occurrence_index.py +327 -0
- codenib/scip_interface/rust_analyzer.py +29 -0
- codenib/scip_interface/scip-environment.yml +14 -0
- codenib/scip_interface/scip.proto +890 -0
- codenib/scip_interface/scip_decode_core.py +190 -0
- codenib/scip_interface/scip_decode_csharp.py +143 -0
- codenib/scip_interface/scip_decode_go.py +433 -0
- codenib/scip_interface/scip_decode_java.py +1117 -0
- codenib/scip_interface/scip_decode_php.py +194 -0
- codenib/scip_interface/scip_decode_python.py +400 -0
- codenib/scip_interface/scip_decode_ruby.py +464 -0
- codenib/scip_interface/scip_decode_rust.py +584 -0
- codenib/scip_interface/scip_decode_ts.py +542 -0
- codenib/scip_interface/scip_decode_utils.py +52 -0
- codenib/scip_interface/scip_indexer_base.py +728 -0
- codenib/scip_interface/scip_indexer_csharp.py +166 -0
- codenib/scip_interface/scip_indexer_go.py +164 -0
- codenib/scip_interface/scip_indexer_java.py +201 -0
- codenib/scip_interface/scip_indexer_php.py +548 -0
- codenib/scip_interface/scip_indexer_python.py +500 -0
- codenib/scip_interface/scip_indexer_ruby.py +369 -0
- codenib/scip_interface/scip_indexer_rust.py +183 -0
- codenib/scip_interface/scip_indexer_ts.py +616 -0
- codenib/scip_interface/scip_install.sh +8 -0
- codenib/scip_interface/scip_pb2.py +80 -0
- codenib/search.py +371 -0
- codenib/source_fingerprint.py +198 -0
- codenib/types.py +105 -0
- codenib/utils.py +93 -0
- codenib/web/__init__.py +9 -0
- codenib/web/app.py +467 -0
- codenib/web/codemap.py +753 -0
- codenib/web/commit_window.py +296 -0
- codenib/web/config.py +331 -0
- codenib/web/edge_label.py +332 -0
- codenib/web/frontend/assets/AskBar-Q_2zSw_D.js +31 -0
- codenib/web/frontend/assets/CodeGraph-C5zIOx4j.js +3 -0
- codenib/web/frontend/assets/CodePanel-BE7Wg1wI.js +1 -0
- codenib/web/frontend/assets/Codemap-C5Xm7Go_.js +2 -0
- codenib/web/frontend/assets/GraphView-CBBbxS9a.js +2 -0
- codenib/web/frontend/assets/Header-CUW7gfQl.js +1 -0
- codenib/web/frontend/assets/HighlightedBlock-H7FGwbAI.js +1 -0
- codenib/web/frontend/assets/HighlightedCode-CTRFPFV9.js +2 -0
- codenib/web/frontend/assets/Mermaid-NhydXLyh.js +303 -0
- codenib/web/frontend/assets/api-DMyba6Hn.js +1 -0
- codenib/web/frontend/assets/arc-Cj6TUG7b.js +1 -0
- codenib/web/frontend/assets/architectureDiagram-3BPJPVTR-BXC7SbpZ.js +36 -0
- codenib/web/frontend/assets/blockDiagram-GPEHLZMM-BYhhgH_O.js +132 -0
- codenib/web/frontend/assets/c4Diagram-AAUBKEIU-BPe9Xb1K.js +10 -0
- codenib/web/frontend/assets/channel-DGptmZnr.js +1 -0
- codenib/web/frontend/assets/chunk-2J33WTMH-mJuoTx1I.js +1 -0
- codenib/web/frontend/assets/chunk-4BX2VUAB-DyIPfnHV.js +1 -0
- codenib/web/frontend/assets/chunk-55IACEB6-C8bSn9Qq.js +1 -0
- codenib/web/frontend/assets/chunk-727SXJPM-CgiBqZFm.js +206 -0
- codenib/web/frontend/assets/chunk-AQP2D5EJ-DOOlLotN.js +231 -0
- codenib/web/frontend/assets/chunk-FMBD7UC4-mf47mSz2.js +15 -0
- codenib/web/frontend/assets/chunk-ND2GUHAM-D6lobpla.js +1 -0
- codenib/web/frontend/assets/chunk-QZHKN3VN-D9a00sWs.js +1 -0
- codenib/web/frontend/assets/classDiagram-4FO5ZUOK-wDtvnBsq.js +1 -0
- codenib/web/frontend/assets/classDiagram-v2-Q7XG4LA2-wDtvnBsq.js +1 -0
- codenib/web/frontend/assets/cose-bilkent-S5V4N54A-JwC1FU9v.js +1 -0
- codenib/web/frontend/assets/cytoscape.esm-CkSuTymj.js +321 -0
- codenib/web/frontend/assets/dagre-BM42HDAG-BTm6Sb-x.js +4 -0
- codenib/web/frontend/assets/defaultLocale-DX6XiGOO.js +1 -0
- codenib/web/frontend/assets/diagram-2AECGRRQ-C7XNwfvk.js +43 -0
- codenib/web/frontend/assets/diagram-5GNKFQAL-DcLriIgZ.js +10 -0
- codenib/web/frontend/assets/diagram-KO2AKTUF-Bo-6Bpfo.js +3 -0
- codenib/web/frontend/assets/diagram-LMA3HP47-M4Nh-qq-.js +24 -0
- codenib/web/frontend/assets/diagram-OG6HWLK6-BUT1QVEf.js +24 -0
- codenib/web/frontend/assets/erDiagram-TEJ5UH35-DViEMoeP.js +85 -0
- codenib/web/frontend/assets/flowDiagram-I6XJVG4X-DjBB1gSM.js +162 -0
- codenib/web/frontend/assets/ganttDiagram-6RSMTGT7-Drhb89TN.js +292 -0
- codenib/web/frontend/assets/gitGraphDiagram-PVQCEYII-CwqblmoY.js +106 -0
- codenib/web/frontend/assets/graph--OzhPTMs.js +1 -0
- codenib/web/frontend/assets/highlight-CDab0zVI.css +1 -0
- codenib/web/frontend/assets/highlight-CnfLc-V2.js +5 -0
- codenib/web/frontend/assets/index-BHP8c3Tq.js +9 -0
- codenib/web/frontend/assets/index-CKJrqu86.css +1 -0
- codenib/web/frontend/assets/infoDiagram-5YYISTIA-BKq62LUG.js +2 -0
- codenib/web/frontend/assets/init-Gi6I4Gst.js +1 -0
- codenib/web/frontend/assets/ishikawaDiagram-YF4QCWOH-DBAgfp9d.js +70 -0
- codenib/web/frontend/assets/journeyDiagram-JHISSGLW-DwHStEV9.js +139 -0
- codenib/web/frontend/assets/kanban-definition-UN3LZRKU-CWPLLTQn.js +89 -0
- codenib/web/frontend/assets/katex-HP8lGamR.js +257 -0
- codenib/web/frontend/assets/layout-SsrduOYp.js +1 -0
- codenib/web/frontend/assets/linear-DNcGZFnN.js +1 -0
- codenib/web/frontend/assets/mindmap-definition-RKZ34NQL-FZHYTpdO.js +96 -0
- codenib/web/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
- codenib/web/frontend/assets/page-Bn-Q8MBy.js +6 -0
- codenib/web/frontend/assets/page-CESv5Ezw.js +2 -0
- codenib/web/frontend/assets/page-CHsElUu2.js +1 -0
- codenib/web/frontend/assets/page-D1WhIRzi.js +1 -0
- codenib/web/frontend/assets/pieDiagram-4H26LBE5-DKlC7xtb.js +30 -0
- codenib/web/frontend/assets/quadrantDiagram-W4KKPZXB-DldkGUZS.js +7 -0
- codenib/web/frontend/assets/requirementDiagram-4Y6WPE33-Dndb6w-j.js +84 -0
- codenib/web/frontend/assets/sankeyDiagram-5OEKKPKP-B2ewtJAC.js +40 -0
- codenib/web/frontend/assets/sequenceDiagram-3UESZ5HK-2N2KbYFt.js +162 -0
- codenib/web/frontend/assets/stateDiagram-AJRCARHV-CLwUuVlz.js +1 -0
- codenib/web/frontend/assets/stateDiagram-v2-BHNVJYJU-IiZdW_Og.js +1 -0
- codenib/web/frontend/assets/timeline-definition-PNZ67QCA-D_IdllIp.js +120 -0
- codenib/web/frontend/assets/vennDiagram-CIIHVFJN-CSU7pI9M.js +34 -0
- codenib/web/frontend/assets/wardley-L42UT6IY-Bv1Eg1Al.js +161 -0
- codenib/web/frontend/assets/wardleyDiagram-YWT4CUSO-Di3bYwLm.js +78 -0
- codenib/web/frontend/assets/xychartDiagram-2RQKCTM6-CPHWEBiN.js +7 -0
- codenib/web/frontend/codenib-icon.svg +34 -0
- codenib/web/frontend/index.html +29 -0
- codenib/web/frontend/runtime-config.js +1 -0
- codenib/web/launcher.py +252 -0
- codenib/web/local.py +196 -0
- codenib/web/repo_registry.py +561 -0
- codenib/web/schemas.py +329 -0
- codenib/web/static_server.py +213 -0
- codenib/wiki/__init__.py +15 -0
- codenib/wiki/agent_wiki.py +4665 -0
- codenib/wiki/builder.py +849 -0
- codenib/wiki/evidence.py +905 -0
- codenib/wiki/narrator.py +258 -0
- codenib/wiki/outline.py +1432 -0
- codenib/wiki/quality.py +932 -0
- codenib-0.1.0.dist-info/METADATA +293 -0
- codenib-0.1.0.dist-info/RECORD +386 -0
- codenib-0.1.0.dist-info/WHEEL +5 -0
- codenib-0.1.0.dist-info/entry_points.txt +13 -0
- codenib-0.1.0.dist-info/licenses/LICENSE +201 -0
- codenib-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Keyword extraction agent for problem statements.
|
|
7
|
+
This module extracts key terms from problem statements using llama_index.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from ..llm.litellm_chat import LiteLLMChat, human_message
|
|
15
|
+
from ..log_utils import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Define Pydantic model for structured output
|
|
21
|
+
class KeywordExtraction(BaseModel):
|
|
22
|
+
"""Model for keyword extraction output."""
|
|
23
|
+
|
|
24
|
+
keywords: List[str] = Field(description="List of extracted keywords")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KeywordExtractor:
|
|
28
|
+
"""Agent for extracting keywords from problem statements."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
llm: LiteLLMChat,
|
|
33
|
+
):
|
|
34
|
+
"""Initialize the keyword extractor.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
llm: A configured LiteLLMChat instance.
|
|
38
|
+
"""
|
|
39
|
+
self.llm = llm
|
|
40
|
+
self.structured_llm = self.llm.with_structured_output(KeywordExtraction)
|
|
41
|
+
|
|
42
|
+
def extract_keywords(self, problem_statement: str) -> KeywordExtraction:
|
|
43
|
+
"""
|
|
44
|
+
Extract keywords from a problem statement.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
problem_statement (str): The problem statement to extract keywords from
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
KeywordExtraction: Structured output with extracted keywords
|
|
51
|
+
"""
|
|
52
|
+
# Create prompt with detailed instructions
|
|
53
|
+
prompt = (
|
|
54
|
+
"You are a keyword extraction specialist. "
|
|
55
|
+
"Your task is to extract important keywords "
|
|
56
|
+
"from problem statements. Focus on identifying "
|
|
57
|
+
"technical terms, function names, class names, "
|
|
58
|
+
"modules, file paths, and concepts that would be "
|
|
59
|
+
"useful for searching in a codebase. "
|
|
60
|
+
"\n\n"
|
|
61
|
+
"Guidelines for extraction:\n"
|
|
62
|
+
"1. Extract file paths and file names "
|
|
63
|
+
"(e.g., 'django/db/models/expressions.py'"
|
|
64
|
+
"-> and 'expressions.py')\n"
|
|
65
|
+
"2. Extract function and method names "
|
|
66
|
+
"(e.g., 'separability_matrix', 'run_validators')\n"
|
|
67
|
+
"3. Extract class names and module names\n"
|
|
68
|
+
"4. Prefer precise terms over general ones\n"
|
|
69
|
+
"5. Remove common stopwords and general "
|
|
70
|
+
"programming terms\n"
|
|
71
|
+
"\n\n"
|
|
72
|
+
"Please extract the key technical terms and "
|
|
73
|
+
"concepts from the following problem statement:"
|
|
74
|
+
f"\n\n{problem_statement}\n\n"
|
|
75
|
+
"Return only the essential terms that would be "
|
|
76
|
+
"most useful for searching in a codebase."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Use structured LLM to get output directly as a KeywordExtraction object
|
|
80
|
+
input_msg = human_message(prompt)
|
|
81
|
+
result = self.structured_llm.invoke([input_msg])
|
|
82
|
+
logger.debug(f"Extracted keywords: {result}")
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_keywords_from_statement(
|
|
87
|
+
problem_statement: str,
|
|
88
|
+
llm: LiteLLMChat,
|
|
89
|
+
) -> KeywordExtraction:
|
|
90
|
+
"""
|
|
91
|
+
Extract keywords from a problem statement.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
problem_statement: The problem statement to extract keywords from.
|
|
95
|
+
llm: A configured LiteLLMChat instance.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
KeywordExtraction: Structured output with extracted keywords
|
|
99
|
+
"""
|
|
100
|
+
extractor = KeywordExtractor(llm=llm)
|
|
101
|
+
return extractor.extract_keywords(problem_statement)
|
codenib/agent/harness.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Declarative agent harness configuration.
|
|
6
|
+
|
|
7
|
+
The runner owns execution. This module owns the reusable contract that says
|
|
8
|
+
which tools, prompts, and loop controls a harness exposes. Dataset selection,
|
|
9
|
+
benchmark scoring, model-specific routing, and experiment policy stay outside
|
|
10
|
+
this module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from dataclasses import dataclass, field, fields
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import (
|
|
20
|
+
Any,
|
|
21
|
+
Collection,
|
|
22
|
+
Dict,
|
|
23
|
+
FrozenSet,
|
|
24
|
+
Iterable,
|
|
25
|
+
Iterator,
|
|
26
|
+
List,
|
|
27
|
+
Mapping,
|
|
28
|
+
Optional,
|
|
29
|
+
Union,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ..llm.usage import TokenUsage
|
|
33
|
+
from .route_context import normalize_lsp_route_seed_policy
|
|
34
|
+
|
|
35
|
+
USAGE_TOTAL_KEYS = (
|
|
36
|
+
"prompt_tokens",
|
|
37
|
+
"completion_tokens",
|
|
38
|
+
"total_tokens",
|
|
39
|
+
"cost_usd",
|
|
40
|
+
"cache_read_input_tokens",
|
|
41
|
+
"cache_creation_input_tokens",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def agent_working_directory(cwd: Optional[Union[str, Path]]) -> Iterator[None]:
|
|
47
|
+
"""Temporarily run agent default tools relative to *cwd*.
|
|
48
|
+
|
|
49
|
+
The built-in read/grep/glob tools intentionally accept repo-relative paths.
|
|
50
|
+
Harnesses that run against many repos should scope the process cwd around
|
|
51
|
+
each runner invocation and always restore it afterwards.
|
|
52
|
+
"""
|
|
53
|
+
if cwd is None:
|
|
54
|
+
yield
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
previous = os.getcwd()
|
|
58
|
+
os.chdir(cwd)
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
os.chdir(previous)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_agent_in_directory(
|
|
66
|
+
runner: Any,
|
|
67
|
+
prompt: str,
|
|
68
|
+
cwd: Optional[Union[str, Path]],
|
|
69
|
+
) -> Any:
|
|
70
|
+
"""Run an ``AgentRunner``-like object with repo-relative default tools."""
|
|
71
|
+
with agent_working_directory(cwd):
|
|
72
|
+
return runner.run(prompt)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_names(
|
|
76
|
+
values: Optional[Iterable[str]],
|
|
77
|
+
*,
|
|
78
|
+
field_name: str,
|
|
79
|
+
empty_is_none: bool,
|
|
80
|
+
) -> Optional[FrozenSet[str]]:
|
|
81
|
+
if values is None:
|
|
82
|
+
return None
|
|
83
|
+
names = []
|
|
84
|
+
for value in values:
|
|
85
|
+
if not isinstance(value, str):
|
|
86
|
+
raise TypeError(f"{field_name} entries must be strings")
|
|
87
|
+
name = value.strip()
|
|
88
|
+
if not name:
|
|
89
|
+
raise ValueError(f"{field_name} entries must be non-empty strings")
|
|
90
|
+
names.append(name)
|
|
91
|
+
if not names and empty_is_none:
|
|
92
|
+
return None
|
|
93
|
+
return frozenset(names)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class AgentHarnessSpec:
|
|
98
|
+
"""Stable runner-facing harness contract.
|
|
99
|
+
|
|
100
|
+
This deliberately mirrors generic :class:`AgentRunner` controls only. It
|
|
101
|
+
does not encode benchmark arms, scorer fields, model names, or dataset
|
|
102
|
+
identifiers, so experiments can compare policies without leaking those
|
|
103
|
+
policies into the core runtime API.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
max_turns: int = 10
|
|
107
|
+
max_context_tokens: Optional[int] = None
|
|
108
|
+
allow_skills: Optional[Collection[str]] = None
|
|
109
|
+
exclude_skills: Optional[Collection[str]] = None
|
|
110
|
+
include_default_tools: bool = True
|
|
111
|
+
default_tool_ids: Optional[Collection[str]] = None
|
|
112
|
+
system_prompt: Optional[str] = None
|
|
113
|
+
first_turn_tool_choice: Optional[str] = None
|
|
114
|
+
force_first_turn_only: bool = False
|
|
115
|
+
force_localization_contract: bool = False
|
|
116
|
+
compact_after_read: bool = False
|
|
117
|
+
compact_keep_reads: int = 0
|
|
118
|
+
enable_lsp_route_context: bool = False
|
|
119
|
+
lsp_route_seed_limit: int = 8
|
|
120
|
+
lsp_route_seed_policy: str = "all"
|
|
121
|
+
lsp_route_query_fallback: bool = False
|
|
122
|
+
lsp_route_top_k: int = 12
|
|
123
|
+
lsp_route_include_neighbors: bool = True
|
|
124
|
+
|
|
125
|
+
def __post_init__(self) -> None:
|
|
126
|
+
if self.max_turns <= 0:
|
|
127
|
+
raise ValueError("max_turns must be positive")
|
|
128
|
+
if self.max_context_tokens is not None and self.max_context_tokens <= 0:
|
|
129
|
+
raise ValueError("max_context_tokens must be positive when set")
|
|
130
|
+
if self.compact_keep_reads < 0:
|
|
131
|
+
raise ValueError("compact_keep_reads must be non-negative")
|
|
132
|
+
if self.lsp_route_seed_limit <= 0:
|
|
133
|
+
raise ValueError("lsp_route_seed_limit must be positive")
|
|
134
|
+
if self.lsp_route_top_k <= 0:
|
|
135
|
+
raise ValueError("lsp_route_top_k must be positive")
|
|
136
|
+
object.__setattr__(
|
|
137
|
+
self,
|
|
138
|
+
"lsp_route_seed_policy",
|
|
139
|
+
normalize_lsp_route_seed_policy(self.lsp_route_seed_policy),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
object.__setattr__(
|
|
143
|
+
self,
|
|
144
|
+
"allow_skills",
|
|
145
|
+
_normalize_names(
|
|
146
|
+
self.allow_skills,
|
|
147
|
+
field_name="allow_skills",
|
|
148
|
+
empty_is_none=False,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
object.__setattr__(
|
|
152
|
+
self,
|
|
153
|
+
"exclude_skills",
|
|
154
|
+
_normalize_names(
|
|
155
|
+
self.exclude_skills,
|
|
156
|
+
field_name="exclude_skills",
|
|
157
|
+
empty_is_none=True,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
object.__setattr__(
|
|
161
|
+
self,
|
|
162
|
+
"default_tool_ids",
|
|
163
|
+
_normalize_names(
|
|
164
|
+
self.default_tool_ids,
|
|
165
|
+
field_name="default_tool_ids",
|
|
166
|
+
empty_is_none=True,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def with_overrides(self, **overrides: Any) -> "AgentHarnessSpec":
|
|
171
|
+
"""Return a copy with selected fields replaced and revalidated."""
|
|
172
|
+
data = {field.name: getattr(self, field.name) for field in fields(self)}
|
|
173
|
+
unknown = set(overrides) - set(data)
|
|
174
|
+
if unknown:
|
|
175
|
+
raise TypeError(f"Unknown harness option(s): {sorted(unknown)}")
|
|
176
|
+
data.update(overrides)
|
|
177
|
+
return type(self)(**data)
|
|
178
|
+
|
|
179
|
+
def to_runner_kwargs(
|
|
180
|
+
self,
|
|
181
|
+
*,
|
|
182
|
+
session_ctx: Optional[Any] = None,
|
|
183
|
+
manifest: Optional[Any] = None,
|
|
184
|
+
compile_table: Optional[Any] = None,
|
|
185
|
+
extra: Optional[Mapping[str, Any]] = None,
|
|
186
|
+
) -> Dict[str, Any]:
|
|
187
|
+
"""Return ``AgentRunner`` keyword arguments for this harness.
|
|
188
|
+
|
|
189
|
+
Collection fields are copied into mutable sets because ``AgentRunner``
|
|
190
|
+
treats them as constructor inputs and may derive internal sets from
|
|
191
|
+
them. The spec remains immutable and reusable across cells/subagents.
|
|
192
|
+
"""
|
|
193
|
+
kwargs: Dict[str, Any] = {
|
|
194
|
+
"max_turns": self.max_turns,
|
|
195
|
+
"max_context_tokens": self.max_context_tokens,
|
|
196
|
+
"allow_skills": (
|
|
197
|
+
set(self.allow_skills) if self.allow_skills is not None else None
|
|
198
|
+
),
|
|
199
|
+
"exclude_skills": (
|
|
200
|
+
set(self.exclude_skills) if self.exclude_skills is not None else None
|
|
201
|
+
),
|
|
202
|
+
"include_default_tools": self.include_default_tools,
|
|
203
|
+
"default_tool_ids": (
|
|
204
|
+
set(self.default_tool_ids)
|
|
205
|
+
if self.default_tool_ids is not None
|
|
206
|
+
else None
|
|
207
|
+
),
|
|
208
|
+
"system_prompt": self.system_prompt,
|
|
209
|
+
"first_turn_tool_choice": self.first_turn_tool_choice,
|
|
210
|
+
"force_first_turn_only": self.force_first_turn_only,
|
|
211
|
+
"force_localization_contract": self.force_localization_contract,
|
|
212
|
+
"compact_after_read": self.compact_after_read,
|
|
213
|
+
"compact_keep_reads": self.compact_keep_reads,
|
|
214
|
+
"enable_lsp_route_context": self.enable_lsp_route_context,
|
|
215
|
+
"lsp_route_seed_limit": self.lsp_route_seed_limit,
|
|
216
|
+
"lsp_route_seed_policy": self.lsp_route_seed_policy,
|
|
217
|
+
"lsp_route_query_fallback": self.lsp_route_query_fallback,
|
|
218
|
+
"lsp_route_top_k": self.lsp_route_top_k,
|
|
219
|
+
"lsp_route_include_neighbors": self.lsp_route_include_neighbors,
|
|
220
|
+
"session_ctx": session_ctx,
|
|
221
|
+
"manifest": manifest,
|
|
222
|
+
"compile_table": compile_table,
|
|
223
|
+
}
|
|
224
|
+
if extra:
|
|
225
|
+
kwargs.update(extra)
|
|
226
|
+
return kwargs
|
|
227
|
+
|
|
228
|
+
def create_runner(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
llm: Optional[Any] = None,
|
|
232
|
+
model: Optional[str] = None,
|
|
233
|
+
registry: Optional[Any] = None,
|
|
234
|
+
session_ctx: Optional[Any] = None,
|
|
235
|
+
manifest: Optional[Any] = None,
|
|
236
|
+
compile_table: Optional[Any] = None,
|
|
237
|
+
**overrides: Any,
|
|
238
|
+
) -> Any:
|
|
239
|
+
"""Build an ``AgentRunner`` using this harness.
|
|
240
|
+
|
|
241
|
+
``overrides`` are one-off runner keyword overrides for cases like
|
|
242
|
+
subagents with a shorter turn budget. They do not mutate the spec.
|
|
243
|
+
"""
|
|
244
|
+
from .runner import AgentRunner
|
|
245
|
+
|
|
246
|
+
kwargs = self.to_runner_kwargs(
|
|
247
|
+
session_ctx=session_ctx,
|
|
248
|
+
manifest=manifest,
|
|
249
|
+
compile_table=compile_table,
|
|
250
|
+
extra=overrides,
|
|
251
|
+
)
|
|
252
|
+
return AgentRunner(llm=llm, model=model, registry=registry, **kwargs)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class AgentRunAccumulator:
|
|
257
|
+
"""Accumulate usage and turn counts across related agent runs.
|
|
258
|
+
|
|
259
|
+
A harness may charge one logical cell for several LLM interactions: routing
|
|
260
|
+
gates, isolated subagents, verify retries, or a final convergence run. This
|
|
261
|
+
helper keeps that accounting generic so experiment scripts do not each
|
|
262
|
+
reimplement token/turn summation.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
_usage_values: Dict[str, List[float]] = field(
|
|
266
|
+
default_factory=lambda: {key: [] for key in USAGE_TOTAL_KEYS}
|
|
267
|
+
)
|
|
268
|
+
_turns: List[int] = field(default_factory=list)
|
|
269
|
+
|
|
270
|
+
def add_usage(self, usage: Optional[Any]) -> None:
|
|
271
|
+
"""Add a ``TokenUsage`` or mapping with flat/nested token fields."""
|
|
272
|
+
if usage is None:
|
|
273
|
+
return
|
|
274
|
+
if isinstance(usage, TokenUsage):
|
|
275
|
+
payload: Mapping[str, Any] = usage.to_dict()
|
|
276
|
+
elif isinstance(usage, Mapping):
|
|
277
|
+
nested = usage.get("token_usage")
|
|
278
|
+
payload = nested if isinstance(nested, Mapping) else usage
|
|
279
|
+
else:
|
|
280
|
+
payload = {
|
|
281
|
+
key: getattr(usage, key, None)
|
|
282
|
+
for key in USAGE_TOTAL_KEYS
|
|
283
|
+
if getattr(usage, key, None) is not None
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for key in USAGE_TOTAL_KEYS:
|
|
287
|
+
value = payload.get(key)
|
|
288
|
+
if value is None:
|
|
289
|
+
continue
|
|
290
|
+
try:
|
|
291
|
+
self._usage_values[key].append(float(value))
|
|
292
|
+
except (TypeError, ValueError):
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
def add_turns(self, turns: Optional[int]) -> None:
|
|
296
|
+
"""Add one run's turn count when it is known."""
|
|
297
|
+
if turns is None:
|
|
298
|
+
return
|
|
299
|
+
self._turns.append(int(turns))
|
|
300
|
+
|
|
301
|
+
def add_result(self, result: Any) -> None:
|
|
302
|
+
"""Add accounting fields from an ``AgentResult``-like object."""
|
|
303
|
+
self.add_usage(getattr(result, "usage", None))
|
|
304
|
+
self.add_turns(getattr(result, "total_turns", None))
|
|
305
|
+
|
|
306
|
+
def usage_sum(self, key: str) -> Optional[float]:
|
|
307
|
+
"""Return the sum for one usage field, or ``None`` if never recorded."""
|
|
308
|
+
if key not in self._usage_values:
|
|
309
|
+
raise KeyError(f"Unknown usage field: {key}")
|
|
310
|
+
values = self._usage_values[key]
|
|
311
|
+
if not values:
|
|
312
|
+
return None
|
|
313
|
+
total = sum(values)
|
|
314
|
+
if key != "cost_usd" and total.is_integer():
|
|
315
|
+
return int(total)
|
|
316
|
+
return total
|
|
317
|
+
|
|
318
|
+
def usage_totals(self) -> Dict[str, Optional[float]]:
|
|
319
|
+
"""Return flat totals with ``None`` for fields never observed."""
|
|
320
|
+
return {key: self.usage_sum(key) for key in USAGE_TOTAL_KEYS}
|
|
321
|
+
|
|
322
|
+
def total_turns(self, *, fallback: Optional[int] = None) -> Optional[int]:
|
|
323
|
+
"""Return summed turns, or ``fallback`` when no turn count was added."""
|
|
324
|
+
if not self._turns:
|
|
325
|
+
return fallback
|
|
326
|
+
return sum(self._turns)
|
codenib/agent/history.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Token-budgeted chat history for the agent loop (issue #109, phase 4).
|
|
6
|
+
|
|
7
|
+
The agent loop in :mod:`codenib.agent.runner` appends every assistant
|
|
8
|
+
turn and every tool result to a flat ``messages`` list. On long runs that
|
|
9
|
+
list grows without bound and eventually overflows the model's context
|
|
10
|
+
window.
|
|
11
|
+
|
|
12
|
+
:class:`TokenBudgetedChatHistory` is a drop-in container that caps the
|
|
13
|
+
*total* context tokens. When adding a message would exceed the budget it
|
|
14
|
+
evicts the **oldest non-system messages first**, always preserving:
|
|
15
|
+
|
|
16
|
+
* every ``system`` message (the system prompt — pinned, never evicted), and
|
|
17
|
+
* the most recent messages (the live working set the model needs to act).
|
|
18
|
+
|
|
19
|
+
It is deliberately a plain container (``add_message`` / ``get_messages`` /
|
|
20
|
+
``clear``) so the runner can swap a bare ``list`` for it with no other
|
|
21
|
+
change. Token counting uses ``litellm.token_counter`` when available and
|
|
22
|
+
falls back to a cheap character-based heuristic so the class stays usable
|
|
23
|
+
in unit tests and offline environments.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from ..log_utils import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
# Rough chars-per-token used by the offline heuristic. ~4 chars/token is the
|
|
35
|
+
# conventional English-text approximation; intentionally conservative so the
|
|
36
|
+
# fallback over-counts slightly rather than under-counts (eviction is safer
|
|
37
|
+
# than overflow).
|
|
38
|
+
_CHARS_PER_TOKEN = 4
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _message_text(message: Dict[str, Any]) -> str:
|
|
42
|
+
"""Flatten a chat message dict into the text we count tokens for.
|
|
43
|
+
|
|
44
|
+
Covers the three shapes the runner produces: a plain ``content`` string,
|
|
45
|
+
``content=None`` with ``tool_calls`` (assistant tool requests), and tool
|
|
46
|
+
result messages. Best-effort — anything unrecognized is ``str()``-ified.
|
|
47
|
+
"""
|
|
48
|
+
parts: List[str] = []
|
|
49
|
+
content = message.get("content")
|
|
50
|
+
if isinstance(content, str):
|
|
51
|
+
parts.append(content)
|
|
52
|
+
elif content is not None:
|
|
53
|
+
parts.append(str(content))
|
|
54
|
+
|
|
55
|
+
for tc in message.get("tool_calls") or []:
|
|
56
|
+
fn = (tc or {}).get("function") if isinstance(tc, dict) else None
|
|
57
|
+
if isinstance(fn, dict):
|
|
58
|
+
parts.append(str(fn.get("name", "")))
|
|
59
|
+
parts.append(str(fn.get("arguments", "")))
|
|
60
|
+
return "\n".join(p for p in parts if p)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def count_message_tokens(
|
|
64
|
+
messages: List[Dict[str, Any]],
|
|
65
|
+
*,
|
|
66
|
+
model: Optional[str] = None,
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Best-effort token count for a list of chat messages.
|
|
69
|
+
|
|
70
|
+
Uses ``litellm.token_counter`` (which understands the model's real
|
|
71
|
+
tokenizer and per-message role overhead) when a ``model`` is supplied
|
|
72
|
+
and litellm is importable. Falls back to a ``chars / 4`` heuristic
|
|
73
|
+
otherwise, so this never raises and stays usable in offline tests.
|
|
74
|
+
"""
|
|
75
|
+
if not messages:
|
|
76
|
+
return 0
|
|
77
|
+
if model:
|
|
78
|
+
try:
|
|
79
|
+
import litellm
|
|
80
|
+
|
|
81
|
+
return int(litellm.token_counter(model=model, messages=messages))
|
|
82
|
+
except Exception as exc: # offline / unknown model / litellm bug
|
|
83
|
+
logger.debug("token_counter unavailable (%s); using char heuristic", exc)
|
|
84
|
+
|
|
85
|
+
chars = sum(len(_message_text(m)) for m in messages)
|
|
86
|
+
# +4 per message approximates the role / formatting overhead litellm adds.
|
|
87
|
+
return chars // _CHARS_PER_TOKEN + 4 * len(messages)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TokenBudgetedChatHistory:
|
|
91
|
+
"""Chat-history container that caps total context tokens.
|
|
92
|
+
|
|
93
|
+
Pinned ``system`` messages are always retained. When appending a message
|
|
94
|
+
would push the estimated token total above ``max_tokens``, the oldest
|
|
95
|
+
*non-system* messages are evicted one at a time (oldest first) until the
|
|
96
|
+
history fits again, so the system prompt plus the most recent turns are
|
|
97
|
+
preserved.
|
|
98
|
+
|
|
99
|
+
``keep_last`` guards the tail: the most recent ``keep_last`` non-system
|
|
100
|
+
messages are never evicted even under budget pressure, so the model
|
|
101
|
+
always sees the immediate context it needs to act. A single message that
|
|
102
|
+
on its own exceeds the budget is kept (we never drop the message just
|
|
103
|
+
added) and a warning is logged.
|
|
104
|
+
|
|
105
|
+
The container is intentionally minimal — ``add_message`` /
|
|
106
|
+
``get_messages`` / ``clear`` / ``__len__`` / ``__iter__`` — so the runner
|
|
107
|
+
can use it in place of a bare ``list``.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
max_tokens: int,
|
|
113
|
+
*,
|
|
114
|
+
model: Optional[str] = None,
|
|
115
|
+
keep_last: int = 2,
|
|
116
|
+
) -> None:
|
|
117
|
+
if max_tokens <= 0:
|
|
118
|
+
raise ValueError(f"max_tokens must be positive, got {max_tokens}")
|
|
119
|
+
self.max_tokens = max_tokens
|
|
120
|
+
self.model = model
|
|
121
|
+
self.keep_last = max(0, keep_last)
|
|
122
|
+
self._messages: List[Dict[str, Any]] = []
|
|
123
|
+
|
|
124
|
+
# -- container protocol -------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def __len__(self) -> int:
|
|
127
|
+
return len(self._messages)
|
|
128
|
+
|
|
129
|
+
def __iter__(self):
|
|
130
|
+
return iter(self._messages)
|
|
131
|
+
|
|
132
|
+
def get_messages(self) -> List[Dict[str, Any]]:
|
|
133
|
+
"""Return the live message list (the object the LLM is called with)."""
|
|
134
|
+
return self._messages
|
|
135
|
+
|
|
136
|
+
def clear(self) -> None:
|
|
137
|
+
"""Drop all messages."""
|
|
138
|
+
self._messages.clear()
|
|
139
|
+
|
|
140
|
+
# -- mutation -----------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def add_message(self, message: Dict[str, Any]) -> None:
|
|
143
|
+
"""Append *message*, then evict oldest non-system messages if over budget."""
|
|
144
|
+
self._messages.append(message)
|
|
145
|
+
self._enforce_budget()
|
|
146
|
+
|
|
147
|
+
def extend(self, messages: List[Dict[str, Any]]) -> None:
|
|
148
|
+
"""Append several messages, enforcing the budget after each one."""
|
|
149
|
+
for message in messages:
|
|
150
|
+
self.add_message(message)
|
|
151
|
+
|
|
152
|
+
def total_tokens(self) -> int:
|
|
153
|
+
"""Estimated token total of the current history."""
|
|
154
|
+
return count_message_tokens(self._messages, model=self.model)
|
|
155
|
+
|
|
156
|
+
# -- internals ----------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def _enforce_budget(self) -> None:
|
|
159
|
+
"""Evict oldest non-system messages until within ``max_tokens``.
|
|
160
|
+
|
|
161
|
+
Walks from the front, skipping pinned ``system`` messages and the
|
|
162
|
+
protected ``keep_last`` tail, dropping the oldest eligible message
|
|
163
|
+
each pass until the total fits or nothing more is evictable.
|
|
164
|
+
"""
|
|
165
|
+
while self.total_tokens() > self.max_tokens:
|
|
166
|
+
idx = self._oldest_evictable_index()
|
|
167
|
+
if idx is None:
|
|
168
|
+
# Only system + protected tail remain (or the single message
|
|
169
|
+
# is itself over budget). Keep what we have; the budget is a
|
|
170
|
+
# best-effort cap, not a hard guarantee against a giant turn.
|
|
171
|
+
logger.warning(
|
|
172
|
+
"TokenBudgetedChatHistory: %d tokens still over budget %d "
|
|
173
|
+
"with only system + last %d message(s) left; not evicting "
|
|
174
|
+
"further",
|
|
175
|
+
self.total_tokens(),
|
|
176
|
+
self.max_tokens,
|
|
177
|
+
self.keep_last,
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
evicted = self._messages.pop(idx)
|
|
181
|
+
logger.debug(
|
|
182
|
+
"TokenBudgetedChatHistory: evicted oldest %s message to stay "
|
|
183
|
+
"within %d-token budget",
|
|
184
|
+
evicted.get("role", "?"),
|
|
185
|
+
self.max_tokens,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _oldest_evictable_index(self) -> Optional[int]:
|
|
189
|
+
"""Index of the oldest message that may be evicted, or ``None``.
|
|
190
|
+
|
|
191
|
+
A message is evictable when it is non-system and not inside the
|
|
192
|
+
protected ``keep_last`` tail of non-system messages.
|
|
193
|
+
"""
|
|
194
|
+
n = len(self._messages)
|
|
195
|
+
# The last ``keep_last`` *non-system* messages are pinned (the live
|
|
196
|
+
# working set the model needs to act). Everything older than that and
|
|
197
|
+
# non-system is evictable, oldest first.
|
|
198
|
+
non_system_positions = [
|
|
199
|
+
i for i, m in enumerate(self._messages) if m.get("role") != "system"
|
|
200
|
+
]
|
|
201
|
+
if self.keep_last:
|
|
202
|
+
protected = set(non_system_positions[-self.keep_last :])
|
|
203
|
+
else:
|
|
204
|
+
protected = set()
|
|
205
|
+
for i in range(n):
|
|
206
|
+
m = self._messages[i]
|
|
207
|
+
if m.get("role") == "system":
|
|
208
|
+
continue
|
|
209
|
+
if i in protected:
|
|
210
|
+
continue
|
|
211
|
+
return i
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class PlainChatHistory:
|
|
216
|
+
"""Unbounded chat history with the same interface as the budgeted one.
|
|
217
|
+
|
|
218
|
+
Lets :class:`~codenib.agent.runner.AgentRunner` use a single code path
|
|
219
|
+
whether or not a token budget is configured. Behaviour is identical to a
|
|
220
|
+
bare ``list`` of message dicts — nothing is ever evicted.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(self, messages: Optional[List[Dict[str, Any]]] = None) -> None:
|
|
224
|
+
self._messages: List[Dict[str, Any]] = list(messages or [])
|
|
225
|
+
|
|
226
|
+
def __len__(self) -> int:
|
|
227
|
+
return len(self._messages)
|
|
228
|
+
|
|
229
|
+
def __iter__(self):
|
|
230
|
+
return iter(self._messages)
|
|
231
|
+
|
|
232
|
+
def get_messages(self) -> List[Dict[str, Any]]:
|
|
233
|
+
return self._messages
|
|
234
|
+
|
|
235
|
+
def add_message(self, message: Dict[str, Any]) -> None:
|
|
236
|
+
self._messages.append(message)
|
|
237
|
+
|
|
238
|
+
def extend(self, messages: List[Dict[str, Any]]) -> None:
|
|
239
|
+
self._messages.extend(messages)
|
|
240
|
+
|
|
241
|
+
def clear(self) -> None:
|
|
242
|
+
self._messages.clear()
|