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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- 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)
|
velune/context/budget.py
ADDED
|
@@ -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
|