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.
Files changed (386) hide show
  1. codenib/__init__.py +62 -0
  2. codenib/__main__.py +7 -0
  3. codenib/_lazy.py +36 -0
  4. codenib/_version.py +26 -0
  5. codenib/agent/__init__.py +159 -0
  6. codenib/agent/agent_types.py +40 -0
  7. codenib/agent/boundary.py +129 -0
  8. codenib/agent/compile.py +219 -0
  9. codenib/agent/extract_agent.py +101 -0
  10. codenib/agent/harness.py +326 -0
  11. codenib/agent/history.py +242 -0
  12. codenib/agent/lsp_graph.py +878 -0
  13. codenib/agent/lsp_provider.py +484 -0
  14. codenib/agent/rerank_agent.py +419 -0
  15. codenib/agent/resource_guard.py +90 -0
  16. codenib/agent/route_context.py +391 -0
  17. codenib/agent/runner.py +2005 -0
  18. codenib/agent/runtime/__init__.py +16 -0
  19. codenib/agent/runtime/context.py +189 -0
  20. codenib/agent/runtime/trace.py +101 -0
  21. codenib/agent/skills/__init__.py +25 -0
  22. codenib/agent/skills/_graphnav.py +235 -0
  23. codenib/agent/skills/bm25_search/__init__.py +0 -0
  24. codenib/agent/skills/bm25_search/config.yaml +63 -0
  25. codenib/agent/skills/bm25_search/executor.py +80 -0
  26. codenib/agent/skills/bm25_search/skill.md +47 -0
  27. codenib/agent/skills/code_to_query/__init__.py +0 -0
  28. codenib/agent/skills/code_to_query/config.yaml +38 -0
  29. codenib/agent/skills/code_to_query/executor.py +69 -0
  30. codenib/agent/skills/code_to_query/skill.md +7 -0
  31. codenib/agent/skills/codenib_context/__init__.py +0 -0
  32. codenib/agent/skills/codenib_context/config.yaml +44 -0
  33. codenib/agent/skills/codenib_context/executor.py +184 -0
  34. codenib/agent/skills/codenib_context/skill.md +21 -0
  35. codenib/agent/skills/context.py +107 -0
  36. codenib/agent/skills/core.py +153 -0
  37. codenib/agent/skills/crossencoder_rerank/__init__.py +3 -0
  38. codenib/agent/skills/crossencoder_rerank/config.yaml +41 -0
  39. codenib/agent/skills/crossencoder_rerank/executor.py +62 -0
  40. codenib/agent/skills/crossencoder_rerank/skill.md +7 -0
  41. codenib/agent/skills/embedding_search/__init__.py +0 -0
  42. codenib/agent/skills/embedding_search/config.yaml +53 -0
  43. codenib/agent/skills/embedding_search/executor.py +57 -0
  44. codenib/agent/skills/embedding_search/skill.md +47 -0
  45. codenib/agent/skills/find_callees/__init__.py +0 -0
  46. codenib/agent/skills/find_callees/config.yaml +22 -0
  47. codenib/agent/skills/find_callees/executor.py +23 -0
  48. codenib/agent/skills/find_callees/skill.md +11 -0
  49. codenib/agent/skills/find_callers/__init__.py +0 -0
  50. codenib/agent/skills/find_callers/config.yaml +22 -0
  51. codenib/agent/skills/find_callers/executor.py +23 -0
  52. codenib/agent/skills/find_callers/skill.md +11 -0
  53. codenib/agent/skills/hybrid_search/__init__.py +0 -0
  54. codenib/agent/skills/hybrid_search/config.yaml +38 -0
  55. codenib/agent/skills/hybrid_search/executor.py +132 -0
  56. codenib/agent/skills/hybrid_search/skill.md +53 -0
  57. codenib/agent/skills/llm_rerank/__init__.py +0 -0
  58. codenib/agent/skills/llm_rerank/config.yaml +33 -0
  59. codenib/agent/skills/llm_rerank/executor.py +37 -0
  60. codenib/agent/skills/llm_rerank/skill.md +7 -0
  61. codenib/agent/skills/loader.py +240 -0
  62. codenib/agent/skills/lsp_definition/config.yaml +35 -0
  63. codenib/agent/skills/lsp_definition/executor.py +32 -0
  64. codenib/agent/skills/lsp_definition/skill.md +18 -0
  65. codenib/agent/skills/lsp_references/config.yaml +40 -0
  66. codenib/agent/skills/lsp_references/executor.py +34 -0
  67. codenib/agent/skills/lsp_references/skill.md +17 -0
  68. codenib/agent/skills/lsp_route/config.yaml +35 -0
  69. codenib/agent/skills/lsp_route/executor.py +34 -0
  70. codenib/agent/skills/lsp_route/skill.md +23 -0
  71. codenib/agent/skills/registry.py +136 -0
  72. codenib/agent/skills/repository_search/config.yaml +49 -0
  73. codenib/agent/skills/repository_search/executor.py +512 -0
  74. codenib/agent/skills/repository_search/skill.md +28 -0
  75. codenib/agent/skills/trace/__init__.py +0 -0
  76. codenib/agent/skills/trace/config.yaml +30 -0
  77. codenib/agent/skills/trace/executor.py +27 -0
  78. codenib/agent/skills/trace/skill.md +12 -0
  79. codenib/agent/skills/typecheck.py +302 -0
  80. codenib/agent/tool_schema.py +158 -0
  81. codenib/agent/tools/__init__.py +41 -0
  82. codenib/agent/tools/defaults.py +1045 -0
  83. codenib/agent/tools/spec.py +103 -0
  84. codenib/agent/utils.py +149 -0
  85. codenib/cli.py +1079 -0
  86. codenib/clients/__init__.py +5 -0
  87. codenib/clients/claude_agent.py +534 -0
  88. codenib/clients/codex_agent.py +314 -0
  89. codenib/code_chunker.py +730 -0
  90. codenib/code_chunking/__init__.py +113 -0
  91. codenib/code_chunking/base.py +592 -0
  92. codenib/code_chunking/cpp_chunker.py +278 -0
  93. codenib/code_chunking/csharp_chunker.py +210 -0
  94. codenib/code_chunking/go_chunker.py +213 -0
  95. codenib/code_chunking/java_chunker.py +188 -0
  96. codenib/code_chunking/js_chunker.py +251 -0
  97. codenib/code_chunking/kotlin_chunker.py +253 -0
  98. codenib/code_chunking/lua_chunker.py +94 -0
  99. codenib/code_chunking/php_chunker.py +204 -0
  100. codenib/code_chunking/python_chunker.py +267 -0
  101. codenib/code_chunking/ruby_chunker.py +198 -0
  102. codenib/code_chunking/rust_chunker.py +195 -0
  103. codenib/code_chunking/scala_chunker.py +182 -0
  104. codenib/code_chunking/swift_chunker.py +182 -0
  105. codenib/compat_pickle.py +48 -0
  106. codenib/compiler/__init__.py +93 -0
  107. codenib/compiler/index_builders.py +868 -0
  108. codenib/compiler/index_compiler.py +488 -0
  109. codenib/compiler/manifest.py +221 -0
  110. codenib/compiler/params.py +126 -0
  111. codenib/compiler/resources.py +267 -0
  112. codenib/compiler/skill_context.py +641 -0
  113. codenib/compiler/snapshot_store.py +311 -0
  114. codenib/compiler/verification.py +131 -0
  115. codenib/dataset/__init__.py +21 -0
  116. codenib/dataset/base.py +78 -0
  117. codenib/dataset/codenib_base.py +374 -0
  118. codenib/dataset/codenib_synthesis.py +540 -0
  119. codenib/dataset/collect/difficulty_classifier.py +511 -0
  120. codenib/dataset/collect/swebench_sample.py +766 -0
  121. codenib/dataset/gt_locate.py +831 -0
  122. codenib/dataset/local_json.py +184 -0
  123. codenib/dataset/locbench.py +254 -0
  124. codenib/dataset/swebench.py +344 -0
  125. codenib/dataset/swebench_multilingual.py +283 -0
  126. codenib/dataset/synthesize/__init__.py +71 -0
  127. codenib/dataset/synthesize/_agent.py +125 -0
  128. codenib/dataset/synthesize/_types.py +146 -0
  129. codenib/dataset/synthesize/context_loader.py +601 -0
  130. codenib/dataset/synthesize/query_curator.py +713 -0
  131. codenib/dataset/synthesize/query_synthesizer.py +546 -0
  132. codenib/dataset/synthesize/verifier.py +425 -0
  133. codenib/dataset/synthesize/vocab_guard.py +264 -0
  134. codenib/dataset/utils.py +380 -0
  135. codenib/eval/agent_runner/__init__.py +296 -0
  136. codenib/eval/agent_runner/baseline.py +299 -0
  137. codenib/eval/agent_runner/batch.py +176 -0
  138. codenib/eval/agent_runner/contexts.py +135 -0
  139. codenib/eval/agent_runner/feedback.py +135 -0
  140. codenib/eval/agent_runner/feedback_summary.py +326 -0
  141. codenib/eval/agent_runner/format_diagnostics.py +115 -0
  142. codenib/eval/agent_runner/live_lsp_provider.py +377 -0
  143. codenib/eval/agent_runner/loc_baseline.py +355 -0
  144. codenib/eval/agent_runner/lsp_agent_ab.py +577 -0
  145. codenib/eval/agent_runner/lsp_agent_study.py +408 -0
  146. codenib/eval/agent_runner/lsp_agent_study_analysis.py +411 -0
  147. codenib/eval/agent_runner/lsp_agent_study_artifacts.py +413 -0
  148. codenib/eval/agent_runner/lsp_agent_study_manifest.py +402 -0
  149. codenib/eval/agent_runner/lsp_agent_study_runner.py +694 -0
  150. codenib/eval/agent_runner/lsp_baseline.py +251 -0
  151. codenib/eval/agent_runner/lsp_latency.py +267 -0
  152. codenib/eval/agent_runner/lsp_provider_cli.py +273 -0
  153. codenib/eval/agent_runner/lsp_provider_validation.py +594 -0
  154. codenib/eval/agent_runner/lsp_readiness.py +167 -0
  155. codenib/eval/agent_runner/lsp_replay_benchmark.py +1072 -0
  156. codenib/eval/agent_runner/metrics.py +80 -0
  157. codenib/eval/agent_runner/orchestrator.py +173 -0
  158. codenib/eval/agent_runner/pareto.py +154 -0
  159. codenib/eval/agent_runner/prebuilt.py +484 -0
  160. codenib/eval/agent_runner/preload.py +219 -0
  161. codenib/eval/agent_runner/promotion.py +172 -0
  162. codenib/eval/agent_runner/query_sweep.py +432 -0
  163. codenib/eval/agent_runner/results.py +69 -0
  164. codenib/eval/agent_runner/scoring.py +134 -0
  165. codenib/eval/agent_runner/sweep.py +619 -0
  166. codenib/eval/agent_runner/sweep_config.py +160 -0
  167. codenib/eval/agent_runner/symbols.py +67 -0
  168. codenib/eval/agent_runner/trace_summary.py +405 -0
  169. codenib/eval/agent_runner/verify_expand.py +219 -0
  170. codenib/eval/artifact_bundle.py +322 -0
  171. codenib/eval/artifact_integrity.py +332 -0
  172. codenib/eval/artifact_manifest.py +236 -0
  173. codenib/eval/experiments/__init__.py +5 -0
  174. codenib/eval/experiments/lsp_agent_study_policy.py +55 -0
  175. codenib/eval/loc_agent_runner.py +37 -0
  176. codenib/eval/reports/__init__.py +5 -0
  177. codenib/eval/reports/cost_arm_report.py +810 -0
  178. codenib/eval/retrieval_eval.py +557 -0
  179. codenib/graph/__init__.py +44 -0
  180. codenib/graph/backend_alignment.py +184 -0
  181. codenib/graph/code_graph.py +1218 -0
  182. codenib/graph/dependency.py +229 -0
  183. codenib/graph/hierarchy.py +828 -0
  184. codenib/graph/incremental/__init__.py +15 -0
  185. codenib/graph/incremental/change_mgr.py +165 -0
  186. codenib/graph/incremental/graph_patcher.py +139 -0
  187. codenib/graph/incremental/lsp_client.py +1271 -0
  188. codenib/graph/incremental/patcher_base.py +1364 -0
  189. codenib/graph/incremental/patcher_cpp.py +796 -0
  190. codenib/graph/incremental/patcher_go.py +56 -0
  191. codenib/graph/incremental/patcher_python.py +43 -0
  192. codenib/graph/incremental/patcher_rust.py +113 -0
  193. codenib/graph/incremental/patcher_ts.py +49 -0
  194. codenib/graph/incremental/subgraph_mgr.py +1028 -0
  195. codenib/graph/layers.py +325 -0
  196. codenib/graph/roi_subgraph.py +394 -0
  197. codenib/graph/setup.py +689 -0
  198. codenib/graph/traverse_graph.py +237 -0
  199. codenib/index/__init__.py +42 -0
  200. codenib/index/embedding/__init__.py +48 -0
  201. codenib/index/embedding/builders.py +190 -0
  202. codenib/index/embedding/model_policy.py +62 -0
  203. codenib/index/embedding/prompt_registry.py +87 -0
  204. codenib/index/embedding/vector_store.py +1458 -0
  205. codenib/index/incremental/__init__.py +30 -0
  206. codenib/index/incremental/chunk_store.py +409 -0
  207. codenib/index/incremental/embeddings_cache.py +196 -0
  208. codenib/index/incremental/git_diff.py +221 -0
  209. codenib/index/incremental/index_updater.py +283 -0
  210. codenib/index/incremental/state.py +92 -0
  211. codenib/index/regex_idx/__init__.py +7 -0
  212. codenib/index/regex_idx/regex_idx.py +150 -0
  213. codenib/index/rerank/__init__.py +23 -0
  214. codenib/index/rerank/cross_encoder.py +356 -0
  215. codenib/index/sparse_idx/__init__.py +9 -0
  216. codenib/index/sparse_idx/bm25_index.py +724 -0
  217. codenib/index/trigram/__init__.py +17 -0
  218. codenib/index/trigram/zoekt_searcher.py +371 -0
  219. codenib/languages.py +1035 -0
  220. codenib/llm/__init__.py +33 -0
  221. codenib/llm/diagnostics.py +170 -0
  222. codenib/llm/litellm_chat.py +422 -0
  223. codenib/llm/options.py +139 -0
  224. codenib/llm/usage.py +182 -0
  225. codenib/log_utils.py +301 -0
  226. codenib/ls_index/__init__.py +17 -0
  227. codenib/ls_index/clangd_decode.py +912 -0
  228. codenib/ls_index/clangd_indexer.py +1400 -0
  229. codenib/ls_index/index_quality.py +442 -0
  230. codenib/ls_index/lsp_graph_decode.py +449 -0
  231. codenib/ls_index/lsp_indexer.py +246 -0
  232. codenib/ls_router.py +507 -0
  233. codenib/mcp/__init__.py +13 -0
  234. codenib/mcp/__main__.py +9 -0
  235. codenib/mcp/context.py +211 -0
  236. codenib/mcp/prompts.py +76 -0
  237. codenib/mcp/server.py +463 -0
  238. codenib/mcp/tools/__init__.py +9 -0
  239. codenib/mcp/tools/dependency.py +56 -0
  240. codenib/mcp/tools/lsp.py +119 -0
  241. codenib/mcp/tools/search.py +224 -0
  242. codenib/model/__init__.py +49 -0
  243. codenib/model/agentless_pipeline.py +484 -0
  244. codenib/model/bm25_retrieve_pipeline.py +100 -0
  245. codenib/model/dense_graph_expand_rerank_pipeline.py +300 -0
  246. codenib/model/embedding_retrieve_pipeline.py +152 -0
  247. codenib/model/graph_augmented_rerank_pipeline.py +19 -0
  248. codenib/model/graph_retrieve_pipeline.py +259 -0
  249. codenib/model/hybrid_retrieve_pipeline.py +256 -0
  250. codenib/model/retrieval_planner.py +377 -0
  251. codenib/model/retrieve_rerank_pipeline.py +1023 -0
  252. codenib/ops/expand.py +299 -0
  253. codenib/ops/filter.py +159 -0
  254. codenib/ops/rerank.py +246 -0
  255. codenib/ops/retrieve.py +293 -0
  256. codenib/ops/transform.py +34 -0
  257. codenib/paths.py +91 -0
  258. codenib/profiler.py +506 -0
  259. codenib/repository_filters.py +103 -0
  260. codenib/repository_summary.py +161 -0
  261. codenib/scip_interface/__init__.py +120 -0
  262. codenib/scip_interface/lsp_occurrence_index.py +327 -0
  263. codenib/scip_interface/rust_analyzer.py +29 -0
  264. codenib/scip_interface/scip-environment.yml +14 -0
  265. codenib/scip_interface/scip.proto +890 -0
  266. codenib/scip_interface/scip_decode_core.py +190 -0
  267. codenib/scip_interface/scip_decode_csharp.py +143 -0
  268. codenib/scip_interface/scip_decode_go.py +433 -0
  269. codenib/scip_interface/scip_decode_java.py +1117 -0
  270. codenib/scip_interface/scip_decode_php.py +194 -0
  271. codenib/scip_interface/scip_decode_python.py +400 -0
  272. codenib/scip_interface/scip_decode_ruby.py +464 -0
  273. codenib/scip_interface/scip_decode_rust.py +584 -0
  274. codenib/scip_interface/scip_decode_ts.py +542 -0
  275. codenib/scip_interface/scip_decode_utils.py +52 -0
  276. codenib/scip_interface/scip_indexer_base.py +728 -0
  277. codenib/scip_interface/scip_indexer_csharp.py +166 -0
  278. codenib/scip_interface/scip_indexer_go.py +164 -0
  279. codenib/scip_interface/scip_indexer_java.py +201 -0
  280. codenib/scip_interface/scip_indexer_php.py +548 -0
  281. codenib/scip_interface/scip_indexer_python.py +500 -0
  282. codenib/scip_interface/scip_indexer_ruby.py +369 -0
  283. codenib/scip_interface/scip_indexer_rust.py +183 -0
  284. codenib/scip_interface/scip_indexer_ts.py +616 -0
  285. codenib/scip_interface/scip_install.sh +8 -0
  286. codenib/scip_interface/scip_pb2.py +80 -0
  287. codenib/search.py +371 -0
  288. codenib/source_fingerprint.py +198 -0
  289. codenib/types.py +105 -0
  290. codenib/utils.py +93 -0
  291. codenib/web/__init__.py +9 -0
  292. codenib/web/app.py +467 -0
  293. codenib/web/codemap.py +753 -0
  294. codenib/web/commit_window.py +296 -0
  295. codenib/web/config.py +331 -0
  296. codenib/web/edge_label.py +332 -0
  297. codenib/web/frontend/assets/AskBar-Q_2zSw_D.js +31 -0
  298. codenib/web/frontend/assets/CodeGraph-C5zIOx4j.js +3 -0
  299. codenib/web/frontend/assets/CodePanel-BE7Wg1wI.js +1 -0
  300. codenib/web/frontend/assets/Codemap-C5Xm7Go_.js +2 -0
  301. codenib/web/frontend/assets/GraphView-CBBbxS9a.js +2 -0
  302. codenib/web/frontend/assets/Header-CUW7gfQl.js +1 -0
  303. codenib/web/frontend/assets/HighlightedBlock-H7FGwbAI.js +1 -0
  304. codenib/web/frontend/assets/HighlightedCode-CTRFPFV9.js +2 -0
  305. codenib/web/frontend/assets/Mermaid-NhydXLyh.js +303 -0
  306. codenib/web/frontend/assets/api-DMyba6Hn.js +1 -0
  307. codenib/web/frontend/assets/arc-Cj6TUG7b.js +1 -0
  308. codenib/web/frontend/assets/architectureDiagram-3BPJPVTR-BXC7SbpZ.js +36 -0
  309. codenib/web/frontend/assets/blockDiagram-GPEHLZMM-BYhhgH_O.js +132 -0
  310. codenib/web/frontend/assets/c4Diagram-AAUBKEIU-BPe9Xb1K.js +10 -0
  311. codenib/web/frontend/assets/channel-DGptmZnr.js +1 -0
  312. codenib/web/frontend/assets/chunk-2J33WTMH-mJuoTx1I.js +1 -0
  313. codenib/web/frontend/assets/chunk-4BX2VUAB-DyIPfnHV.js +1 -0
  314. codenib/web/frontend/assets/chunk-55IACEB6-C8bSn9Qq.js +1 -0
  315. codenib/web/frontend/assets/chunk-727SXJPM-CgiBqZFm.js +206 -0
  316. codenib/web/frontend/assets/chunk-AQP2D5EJ-DOOlLotN.js +231 -0
  317. codenib/web/frontend/assets/chunk-FMBD7UC4-mf47mSz2.js +15 -0
  318. codenib/web/frontend/assets/chunk-ND2GUHAM-D6lobpla.js +1 -0
  319. codenib/web/frontend/assets/chunk-QZHKN3VN-D9a00sWs.js +1 -0
  320. codenib/web/frontend/assets/classDiagram-4FO5ZUOK-wDtvnBsq.js +1 -0
  321. codenib/web/frontend/assets/classDiagram-v2-Q7XG4LA2-wDtvnBsq.js +1 -0
  322. codenib/web/frontend/assets/cose-bilkent-S5V4N54A-JwC1FU9v.js +1 -0
  323. codenib/web/frontend/assets/cytoscape.esm-CkSuTymj.js +321 -0
  324. codenib/web/frontend/assets/dagre-BM42HDAG-BTm6Sb-x.js +4 -0
  325. codenib/web/frontend/assets/defaultLocale-DX6XiGOO.js +1 -0
  326. codenib/web/frontend/assets/diagram-2AECGRRQ-C7XNwfvk.js +43 -0
  327. codenib/web/frontend/assets/diagram-5GNKFQAL-DcLriIgZ.js +10 -0
  328. codenib/web/frontend/assets/diagram-KO2AKTUF-Bo-6Bpfo.js +3 -0
  329. codenib/web/frontend/assets/diagram-LMA3HP47-M4Nh-qq-.js +24 -0
  330. codenib/web/frontend/assets/diagram-OG6HWLK6-BUT1QVEf.js +24 -0
  331. codenib/web/frontend/assets/erDiagram-TEJ5UH35-DViEMoeP.js +85 -0
  332. codenib/web/frontend/assets/flowDiagram-I6XJVG4X-DjBB1gSM.js +162 -0
  333. codenib/web/frontend/assets/ganttDiagram-6RSMTGT7-Drhb89TN.js +292 -0
  334. codenib/web/frontend/assets/gitGraphDiagram-PVQCEYII-CwqblmoY.js +106 -0
  335. codenib/web/frontend/assets/graph--OzhPTMs.js +1 -0
  336. codenib/web/frontend/assets/highlight-CDab0zVI.css +1 -0
  337. codenib/web/frontend/assets/highlight-CnfLc-V2.js +5 -0
  338. codenib/web/frontend/assets/index-BHP8c3Tq.js +9 -0
  339. codenib/web/frontend/assets/index-CKJrqu86.css +1 -0
  340. codenib/web/frontend/assets/infoDiagram-5YYISTIA-BKq62LUG.js +2 -0
  341. codenib/web/frontend/assets/init-Gi6I4Gst.js +1 -0
  342. codenib/web/frontend/assets/ishikawaDiagram-YF4QCWOH-DBAgfp9d.js +70 -0
  343. codenib/web/frontend/assets/journeyDiagram-JHISSGLW-DwHStEV9.js +139 -0
  344. codenib/web/frontend/assets/kanban-definition-UN3LZRKU-CWPLLTQn.js +89 -0
  345. codenib/web/frontend/assets/katex-HP8lGamR.js +257 -0
  346. codenib/web/frontend/assets/layout-SsrduOYp.js +1 -0
  347. codenib/web/frontend/assets/linear-DNcGZFnN.js +1 -0
  348. codenib/web/frontend/assets/mindmap-definition-RKZ34NQL-FZHYTpdO.js +96 -0
  349. codenib/web/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
  350. codenib/web/frontend/assets/page-Bn-Q8MBy.js +6 -0
  351. codenib/web/frontend/assets/page-CESv5Ezw.js +2 -0
  352. codenib/web/frontend/assets/page-CHsElUu2.js +1 -0
  353. codenib/web/frontend/assets/page-D1WhIRzi.js +1 -0
  354. codenib/web/frontend/assets/pieDiagram-4H26LBE5-DKlC7xtb.js +30 -0
  355. codenib/web/frontend/assets/quadrantDiagram-W4KKPZXB-DldkGUZS.js +7 -0
  356. codenib/web/frontend/assets/requirementDiagram-4Y6WPE33-Dndb6w-j.js +84 -0
  357. codenib/web/frontend/assets/sankeyDiagram-5OEKKPKP-B2ewtJAC.js +40 -0
  358. codenib/web/frontend/assets/sequenceDiagram-3UESZ5HK-2N2KbYFt.js +162 -0
  359. codenib/web/frontend/assets/stateDiagram-AJRCARHV-CLwUuVlz.js +1 -0
  360. codenib/web/frontend/assets/stateDiagram-v2-BHNVJYJU-IiZdW_Og.js +1 -0
  361. codenib/web/frontend/assets/timeline-definition-PNZ67QCA-D_IdllIp.js +120 -0
  362. codenib/web/frontend/assets/vennDiagram-CIIHVFJN-CSU7pI9M.js +34 -0
  363. codenib/web/frontend/assets/wardley-L42UT6IY-Bv1Eg1Al.js +161 -0
  364. codenib/web/frontend/assets/wardleyDiagram-YWT4CUSO-Di3bYwLm.js +78 -0
  365. codenib/web/frontend/assets/xychartDiagram-2RQKCTM6-CPHWEBiN.js +7 -0
  366. codenib/web/frontend/codenib-icon.svg +34 -0
  367. codenib/web/frontend/index.html +29 -0
  368. codenib/web/frontend/runtime-config.js +1 -0
  369. codenib/web/launcher.py +252 -0
  370. codenib/web/local.py +196 -0
  371. codenib/web/repo_registry.py +561 -0
  372. codenib/web/schemas.py +329 -0
  373. codenib/web/static_server.py +213 -0
  374. codenib/wiki/__init__.py +15 -0
  375. codenib/wiki/agent_wiki.py +4665 -0
  376. codenib/wiki/builder.py +849 -0
  377. codenib/wiki/evidence.py +905 -0
  378. codenib/wiki/narrator.py +258 -0
  379. codenib/wiki/outline.py +1432 -0
  380. codenib/wiki/quality.py +932 -0
  381. codenib-0.1.0.dist-info/METADATA +293 -0
  382. codenib-0.1.0.dist-info/RECORD +386 -0
  383. codenib-0.1.0.dist-info/WHEEL +5 -0
  384. codenib-0.1.0.dist-info/entry_points.txt +13 -0
  385. codenib-0.1.0.dist-info/licenses/LICENSE +201 -0
  386. 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)
@@ -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)
@@ -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()