velune-cli 0.9.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 (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,240 @@
1
+ """Context assembly with canonical section ordering and budget enforcement.
2
+
3
+ The ContextAssembler composes chunks into a final context string that respects
4
+ budget constraints, enforces canonical ordering, and prioritizes trimming of
5
+ low-trust, low-priority content.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from collections import defaultdict
12
+ from collections.abc import Sequence
13
+
14
+ from velune.context.budget import ContextBudget
15
+ from velune.context.sections import (
16
+ ContextAssemblyReport,
17
+ ContextChunk,
18
+ ContextSection,
19
+ )
20
+ from velune.context.token_counter import TokenCounter
21
+ from velune.core.types.model import ModelDescriptor
22
+
23
+ logger = logging.getLogger("velune.context.assembler")
24
+
25
+
26
+ class ContextAssembler:
27
+ """Assembles context chunks into final context respecting budget limits.
28
+
29
+ Algorithm:
30
+ 1. Group chunks by section
31
+ 2. Sort groups by ContextSection (canonical order 1-7)
32
+ 3. For RETRIEVED_CONTEXT: trim to fit retrieval_allocation (drop low trust_score)
33
+ 4. For WORKING_MEMORY: trim oldest turns to fit working_memory_allocation
34
+ 5. For REPOSITORY_SNAPSHOT: reduce to architecture summary if > 2000 tokens
35
+ 6. NEVER trim: SYSTEM_PROMPT, ARCHITECTURAL_DRIFT, CURRENT_PROMPT
36
+ 7. Join with clear section separators
37
+ """
38
+
39
+ SECTION_SEPARATOR = "--- SECTION: {name} ({number} of 7) ---"
40
+ SECTION_END = "--- END SECTION ---"
41
+ UNTRIMMED_SECTIONS = {
42
+ ContextSection.SYSTEM_PROMPT,
43
+ ContextSection.ARCHITECTURAL_DRIFT,
44
+ ContextSection.CURRENT_PROMPT,
45
+ }
46
+
47
+ def assemble(
48
+ self,
49
+ chunks: Sequence[ContextChunk],
50
+ budget: ContextBudget,
51
+ model: ModelDescriptor | None = None,
52
+ ) -> tuple[str, ContextAssemblyReport]:
53
+ """Assemble chunks into final context respecting budget.
54
+
55
+ Args:
56
+ chunks: List of ContextChunk objects to assemble
57
+ budget: ContextBudget constraints for this session
58
+ model: Optional ModelDescriptor for token counting (defaults to word count)
59
+
60
+ Returns:
61
+ Tuple of (assembled_context_string, assembly_report)
62
+ """
63
+ if not chunks:
64
+ return "", ContextAssemblyReport(
65
+ total_chunks_received=0,
66
+ total_tokens_requested=budget.usable_tokens,
67
+ total_tokens_assembled=0,
68
+ )
69
+
70
+ # Phase 1: Group chunks by section (preserves insertion order)
71
+ sections_dict: dict[ContextSection, list[ContextChunk]] = defaultdict(list)
72
+ for chunk in chunks:
73
+ sections_dict[chunk.section].append(chunk)
74
+
75
+ # Phase 2: Process each section in canonical order
76
+ assembled_sections: dict[ContextSection, str] = {}
77
+ kept_sections_chunks: dict[ContextSection, list[ContextChunk]] = {}
78
+ total_tokens = 0
79
+ sections_trimmed: dict[ContextSection, int] = {}
80
+
81
+ for section in sorted(ContextSection):
82
+ if section not in sections_dict:
83
+ continue
84
+
85
+ section_chunks = sections_dict[section]
86
+ is_trimmable = section not in self.UNTRIMMED_SECTIONS
87
+
88
+ # Apply section-specific trimming logic
89
+ if section == ContextSection.RETRIEVED_CONTEXT:
90
+ processed_chunks, tokens_trimmed = self._trim_retrieved_context(
91
+ section_chunks, budget.retrieval_allocation, model
92
+ )
93
+ if tokens_trimmed > 0:
94
+ sections_trimmed[section] = tokens_trimmed
95
+ elif section == ContextSection.WORKING_MEMORY:
96
+ processed_chunks, tokens_trimmed = self._trim_working_memory(
97
+ section_chunks, budget.working_memory_allocation, model
98
+ )
99
+ if tokens_trimmed > 0:
100
+ sections_trimmed[section] = tokens_trimmed
101
+ elif section == ContextSection.REPOSITORY_SNAPSHOT:
102
+ processed_chunks = self._trim_repository_snapshot(section_chunks)
103
+ elif is_trimmable:
104
+ # For other trimmable sections, no special logic yet
105
+ processed_chunks = section_chunks
106
+ else:
107
+ # Never trim SYSTEM_PROMPT, ARCHITECTURAL_DRIFT, CURRENT_PROMPT
108
+ processed_chunks = section_chunks
109
+
110
+ if processed_chunks:
111
+ section_content = self._render_section(section, processed_chunks)
112
+ assembled_sections[section] = section_content
113
+ kept_sections_chunks[section] = list(processed_chunks)
114
+ total_tokens += sum(c.token_count for c in processed_chunks)
115
+
116
+ # Phase 3: Render final context with separators
117
+ final_context = self._render_assembled_context(assembled_sections)
118
+
119
+ # Phase 4: Check if final assembly exceeds budget
120
+ if model:
121
+ final_token_count = TokenCounter.count(final_context, model)
122
+ else:
123
+ final_token_count = total_tokens
124
+
125
+ budget_exceeded = final_token_count > budget.usable_tokens
126
+
127
+ if budget_exceeded:
128
+ logger.warning(
129
+ f"Assembled context exceeds budget: "
130
+ f"{final_token_count} > {budget.usable_tokens}; "
131
+ f"dropping lowest-priority RETRIEVED_CONTEXT chunks"
132
+ )
133
+ # Emergency truncation of RETRIEVED_CONTEXT
134
+ if ContextSection.RETRIEVED_CONTEXT in assembled_sections:
135
+ del assembled_sections[ContextSection.RETRIEVED_CONTEXT]
136
+ if ContextSection.RETRIEVED_CONTEXT in kept_sections_chunks:
137
+ del kept_sections_chunks[ContextSection.RETRIEVED_CONTEXT]
138
+ final_context = self._render_assembled_context(assembled_sections)
139
+
140
+ report = ContextAssemblyReport(
141
+ total_chunks_received=len(chunks),
142
+ total_tokens_requested=budget.usable_tokens,
143
+ total_tokens_assembled=final_token_count,
144
+ sections_present=list(assembled_sections.keys()),
145
+ sections_trimmed=sections_trimmed,
146
+ chunks_dropped=len(chunks)
147
+ - sum(len(kept_sections_chunks.get(s, [])) for s in ContextSection),
148
+ budget_exceeded=budget_exceeded,
149
+ )
150
+
151
+ return final_context, report
152
+
153
+ def _trim_retrieved_context(
154
+ self,
155
+ chunks: list[ContextChunk],
156
+ allocation: int,
157
+ model: ModelDescriptor | None = None,
158
+ ) -> tuple[list[ContextChunk], int]:
159
+ """Trim RETRIEVED_CONTEXT to fit allocation.
160
+
161
+ Drops lowest trust_score chunks first. Sorts by trust_score descending
162
+ and keeps chunks until allocation is exceeded.
163
+ """
164
+ if sum(c.token_count for c in chunks) <= allocation:
165
+ return chunks, 0
166
+
167
+ # Sort by trust_score descending (keep high-trust chunks first)
168
+ sorted_chunks = sorted(chunks, key=lambda c: c.trust_score, reverse=True)
169
+
170
+ kept_chunks = []
171
+ tokens_used = 0
172
+ for chunk in sorted_chunks:
173
+ if tokens_used + chunk.token_count <= allocation:
174
+ kept_chunks.append(chunk)
175
+ tokens_used += chunk.token_count
176
+
177
+ tokens_trimmed = sum(c.token_count for c in chunks) - sum(
178
+ c.token_count for c in kept_chunks
179
+ )
180
+ return kept_chunks, tokens_trimmed
181
+
182
+ def _trim_working_memory(
183
+ self,
184
+ chunks: list[ContextChunk],
185
+ allocation: int,
186
+ model: ModelDescriptor | None = None,
187
+ ) -> tuple[list[ContextChunk], int]:
188
+ """Trim WORKING_MEMORY to fit allocation.
189
+
190
+ Removes oldest turns (first chunks) first to preserve recent context.
191
+ """
192
+ if sum(c.token_count for c in chunks) <= allocation:
193
+ return chunks, 0
194
+
195
+ # Keep chunks from the end (most recent turns) first
196
+ kept_chunks = []
197
+ tokens_used = 0
198
+ for chunk in reversed(chunks):
199
+ if tokens_used + chunk.token_count <= allocation:
200
+ kept_chunks.insert(0, chunk)
201
+ tokens_used += chunk.token_count
202
+
203
+ tokens_trimmed = sum(c.token_count for c in chunks) - sum(
204
+ c.token_count for c in kept_chunks
205
+ )
206
+ return kept_chunks, tokens_trimmed
207
+
208
+ def _trim_repository_snapshot(self, chunks: list[ContextChunk]) -> list[ContextChunk]:
209
+ """Reduce REPOSITORY_SNAPSHOT to architecture summary if over 2000 tokens.
210
+
211
+ Currently returns chunks as-is. Future enhancement: extract and preserve
212
+ only high-level architecture info when exceeding 2000 tokens.
213
+ """
214
+ total_tokens = sum(c.token_count for c in chunks)
215
+ if total_tokens > 2000:
216
+ logger.debug(
217
+ f"REPOSITORY_SNAPSHOT exceeds 2000 tokens ({total_tokens}); "
218
+ "consider extracting architecture summary only"
219
+ )
220
+ return chunks
221
+
222
+ def _render_section(self, section: ContextSection, chunks: list[ContextChunk]) -> str:
223
+ """Render a section with header, content, and footer."""
224
+ section_name = section.name
225
+ section_number = section.value
226
+
227
+ header = self.SECTION_SEPARATOR.format(name=section_name, number=section_number)
228
+ content = "\n\n".join(c.content for c in chunks)
229
+ footer = self.SECTION_END
230
+
231
+ return f"{header}\n{content}\n{footer}"
232
+
233
+ def _render_assembled_context(self, sections: dict[ContextSection, str]) -> str:
234
+ """Render all sections in canonical order with separators."""
235
+ rendered = []
236
+ for section in sorted(ContextSection):
237
+ if section in sections:
238
+ rendered.append(sections[section])
239
+
240
+ return "\n\n".join(rendered)
@@ -0,0 +1,97 @@
1
+ """Formal context budget allocation system.
2
+
3
+ Defines token budget constraints for each session mode and allocates
4
+ budget across retrieval, working memory, and system overhead.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from velune.cli.modes import SessionMode
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ContextBudget:
18
+ """Token budget allocation across context sections.
19
+
20
+ Frozen dataclass enforcing immutability once created. Budget is
21
+ split proportionally across retrieval, working memory, system overhead,
22
+ and output reservation based on the active SessionMode.
23
+ """
24
+
25
+ total_tokens: int
26
+ retrieval_allocation: int
27
+ working_memory_allocation: int
28
+ output_reservation: int
29
+ system_allocation: int = 512
30
+
31
+ @classmethod
32
+ def from_mode(cls, mode: SessionMode, model_context_window: int) -> ContextBudget:
33
+ """Create a budget for the given session mode and model context.
34
+
35
+ Args:
36
+ mode: SessionMode (OPTIMUS, NORMAL, or GODLY)
37
+ model_context_window: Model's reported context length in tokens
38
+
39
+ Returns:
40
+ ContextBudget with allocations tuned to the mode's constraints
41
+ """
42
+ from velune.cli.modes import SessionMode
43
+
44
+ # Phase 1: Determine total usable tokens based on mode
45
+ if mode == SessionMode.OPTIMUS:
46
+ total = min(4096, model_context_window)
47
+ elif mode == SessionMode.NORMAL:
48
+ total = min(16384, model_context_window)
49
+ elif mode == SessionMode.GODLY:
50
+ total = model_context_window
51
+ else:
52
+ # Fallback to NORMAL
53
+ total = min(16384, model_context_window)
54
+
55
+ # Phase 2: Reserve output space and system overhead
56
+ system_alloc = 512
57
+ output_reserve = min(2048, total // 4)
58
+ usable = total - output_reserve - system_alloc
59
+
60
+ # Phase 3: Allocate remaining budget proportionally
61
+ # retrieval: 55%, working_memory: 35%
62
+ retrieval_alloc = int(usable * 0.55)
63
+ working_memory_alloc = int(usable * 0.35)
64
+
65
+ return cls(
66
+ total_tokens=total,
67
+ retrieval_allocation=retrieval_alloc,
68
+ working_memory_allocation=working_memory_alloc,
69
+ system_allocation=system_alloc,
70
+ output_reservation=output_reserve,
71
+ )
72
+
73
+ @property
74
+ def usable_tokens(self) -> int:
75
+ """Total tokens available for context (minus output and system)."""
76
+ return self.total_tokens - self.system_allocation - self.output_reservation
77
+
78
+ @property
79
+ def unallocated_tokens(self) -> int:
80
+ """Tokens not explicitly allocated to retrieval or working memory."""
81
+ allocated = (
82
+ self.retrieval_allocation
83
+ + self.working_memory_allocation
84
+ + self.system_allocation
85
+ + self.output_reservation
86
+ )
87
+ return self.total_tokens - allocated
88
+
89
+ def __str__(self) -> str:
90
+ """Human-readable budget summary."""
91
+ return (
92
+ f"ContextBudget(total={self.total_tokens}, "
93
+ f"retrieval={self.retrieval_allocation}, "
94
+ f"working_memory={self.working_memory_allocation}, "
95
+ f"system={self.system_allocation}, "
96
+ f"output_reserve={self.output_reservation})"
97
+ )
@@ -0,0 +1,95 @@
1
+ import re
2
+ from collections import Counter
3
+
4
+
5
+ def extractive_compress(text: str, target_tokens: int) -> str:
6
+ """
7
+ Sentence-importance based extractive compression.
8
+ Preserves: first sentence, last sentence, high-keyword-density sentences.
9
+ Drops: boilerplate, repetitive content, low-information sentences.
10
+ """
11
+ sentences = _split_sentences(text)
12
+ if not sentences:
13
+ return text
14
+
15
+ target_chars = target_tokens * 4 # Rough token→char estimate
16
+ if len(text) <= target_chars:
17
+ return text # Already fits
18
+
19
+ # Score each sentence
20
+ word_freq = Counter(re.findall(r"\w+", text.lower()))
21
+ total_words = sum(word_freq.values())
22
+
23
+ scored = []
24
+ for i, sentence in enumerate(sentences):
25
+ score = _score_sentence(sentence, word_freq, total_words)
26
+ # Boost first and last sentences
27
+ if i == 0:
28
+ score += 0.5
29
+ if i == len(sentences) - 1:
30
+ score += 0.3
31
+ # Boost sentences with code-like content
32
+ if any(c in sentence for c in ("def ", "class ", "import ", "() ->", ":=")):
33
+ score += 0.4
34
+ scored.append((score, i, sentence))
35
+
36
+ # Greedily include highest-scoring sentences until budget exhausted
37
+ scored.sort(reverse=True)
38
+ selected = []
39
+ char_count = 0
40
+ selected_indices = set()
41
+
42
+ for _, idx, sentence in scored:
43
+ if char_count + len(sentence) <= target_chars:
44
+ selected.append((idx, sentence))
45
+ selected_indices.add(idx)
46
+ char_count += len(sentence)
47
+
48
+ # Sort selected sentences back to original order
49
+ selected.sort(key=lambda x: x[0])
50
+ result = " ".join(s for _, s in selected)
51
+
52
+ if len(text) > len(result) + 50:
53
+ result += f"\n[COMPRESSED: {len(text)} → {len(result)} chars]"
54
+
55
+ return result
56
+
57
+
58
+ def _score_sentence(sentence: str, word_freq: Counter, total: int) -> float:
59
+ words = re.findall(r"\w+", sentence.lower())
60
+ if not words:
61
+ return 0.0
62
+ # TF-IDF inspired: sum of term frequencies of uncommon words
63
+ score = sum(1.0 / (word_freq[w] + 1) for w in words if len(w) > 3)
64
+ return score / len(words)
65
+
66
+
67
+ def _split_sentences(text: str) -> list[str]:
68
+ # Split on sentence endings, preserve code blocks intact
69
+ return re.split(r"(?<=[.!?])\s+", text)
70
+
71
+
72
+ def compress_conversation(conversation: list[dict], max_tokens: int) -> list[dict]:
73
+ """Drop oldest conversation turns until the total fits within max_tokens."""
74
+ try:
75
+ from velune.context.window import estimate_tokens
76
+ except ImportError:
77
+
78
+ def estimate_tokens(text: str) -> int: # type: ignore[misc]
79
+ return len(text) // 4
80
+
81
+ if not conversation:
82
+ return conversation
83
+
84
+ total = sum(estimate_tokens(m.get("content", "")) for m in conversation)
85
+ if total <= max_tokens:
86
+ return conversation
87
+
88
+ result = list(conversation)
89
+ while len(result) > 1:
90
+ total = sum(estimate_tokens(m.get("content", "")) for m in result)
91
+ if total <= max_tokens:
92
+ break
93
+ result = result[1:]
94
+
95
+ return result