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,480 @@
1
+ """Prompt format adaptation for different model families.
2
+
3
+ Adapts prompts and messages to match the native format of each model family,
4
+ improving output quality without requiring model calls. Each family has a
5
+ distinct prompt structure and system prompt handling.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from abc import ABC
12
+ from typing import Protocol
13
+
14
+ from velune.context.window import estimate_tokens
15
+ from velune.core.types.model import ModelDescriptor
16
+ from velune.models.family import ModelFamily, detect_family
17
+
18
+ logger = logging.getLogger("velune.context.prompt_adaptation")
19
+
20
+
21
+ class PromptTemplate(Protocol):
22
+ """Protocol for prompt format templates.
23
+
24
+ Each model family has a distinct prompt structure.
25
+ Templates handle formatting messages and converting between formats.
26
+ """
27
+
28
+ family: ModelFamily
29
+
30
+ # Guidance flags
31
+ prefer_shorter_system_prompts: bool
32
+ supports_xml_structured_output: bool
33
+ supports_json_mode: bool
34
+ max_recommended_system_tokens: int
35
+
36
+ def format_system(self, system_content: str) -> str:
37
+ """Format a system prompt according to this family's convention."""
38
+ ...
39
+
40
+ def format_user(self, user_content: str) -> str:
41
+ """Format a user message according to this family's convention."""
42
+ ...
43
+
44
+ def format_assistant(self, assistant_content: str) -> str:
45
+ """Format an assistant message according to this family's convention."""
46
+ ...
47
+
48
+ def format_full_conversation(self, system: str, turns: list[dict[str, str]]) -> str:
49
+ """Format a complete conversation into a single prompt string.
50
+
51
+ Used for models that expect a single text input rather than
52
+ message lists. Turns are dicts with 'role' and 'content' keys.
53
+ """
54
+ ...
55
+
56
+
57
+ # ─────────────────────────────────────────────────────────────────────────────
58
+ # Concrete Template Implementations
59
+ # ─────────────────────────────────────────────────────────────────────────────
60
+
61
+
62
+ class QwenTemplate(ABC):
63
+ """Qwen models use ChatML format with <|im_start|>/<|im_end|> tokens."""
64
+
65
+ family = ModelFamily.QWEN
66
+ prefer_shorter_system_prompts = False
67
+ supports_xml_structured_output = True
68
+ supports_json_mode = True
69
+ max_recommended_system_tokens = 4096
70
+
71
+ @staticmethod
72
+ def format_system(system_content: str) -> str:
73
+ return f"<|im_start|>system\n{system_content}\n<|im_end|>"
74
+
75
+ @staticmethod
76
+ def format_user(user_content: str) -> str:
77
+ return f"<|im_start|>user\n{user_content}\n<|im_end|>"
78
+
79
+ @staticmethod
80
+ def format_assistant(assistant_content: str) -> str:
81
+ return f"<|im_start|>assistant\n{assistant_content}\n<|im_end|>"
82
+
83
+ @staticmethod
84
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
85
+ parts = []
86
+ if system:
87
+ parts.append(QwenTemplate.format_system(system))
88
+ for turn in turns:
89
+ role = turn.get("role", "user")
90
+ content = turn.get("content", "")
91
+ if role == "user":
92
+ parts.append(QwenTemplate.format_user(content))
93
+ elif role == "assistant":
94
+ parts.append(QwenTemplate.format_assistant(content))
95
+ elif role == "system":
96
+ parts.append(QwenTemplate.format_system(content))
97
+ return "\n".join(parts) + "\n<|im_start|>assistant\n"
98
+
99
+
100
+ class DeepSeekTemplate(ABC):
101
+ """DeepSeek models support reasoning with special thinking tokens."""
102
+
103
+ family = ModelFamily.DEEPSEEK
104
+ prefer_shorter_system_prompts = False
105
+ supports_xml_structured_output = True
106
+ supports_json_mode = True
107
+ max_recommended_system_tokens = 4096
108
+
109
+ @staticmethod
110
+ def format_system(system_content: str) -> str:
111
+ return f"System: {system_content}"
112
+
113
+ @staticmethod
114
+ def format_user(user_content: str) -> str:
115
+ return f"User: {user_content}"
116
+
117
+ @staticmethod
118
+ def format_assistant(assistant_content: str) -> str:
119
+ return f"Assistant: {assistant_content}"
120
+
121
+ @staticmethod
122
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
123
+ parts = []
124
+ if system:
125
+ parts.append(DeepSeekTemplate.format_system(system))
126
+ for turn in turns:
127
+ role = turn.get("role", "user")
128
+ content = turn.get("content", "")
129
+ if role == "user":
130
+ parts.append(DeepSeekTemplate.format_user(content))
131
+ elif role == "assistant":
132
+ parts.append(DeepSeekTemplate.format_assistant(content))
133
+ elif role == "system":
134
+ parts.append(DeepSeekTemplate.format_system(content))
135
+ return "\n\n".join(parts) + "\n\nAssistant: "
136
+
137
+
138
+ class Llama3Template(ABC):
139
+ """Llama3 uses [INST] / <<SYS>> markers for instruction and system prompts."""
140
+
141
+ family = ModelFamily.LLAMA3
142
+ prefer_shorter_system_prompts = False
143
+ supports_xml_structured_output = True
144
+ supports_json_mode = False
145
+ max_recommended_system_tokens = 2048
146
+
147
+ @staticmethod
148
+ def format_system(system_content: str) -> str:
149
+ return f"<<SYS>>\n{system_content}\n<</SYS>>"
150
+
151
+ @staticmethod
152
+ def format_user(user_content: str) -> str:
153
+ return f"[INST] {user_content} [/INST]"
154
+
155
+ @staticmethod
156
+ def format_assistant(assistant_content: str) -> str:
157
+ return assistant_content
158
+
159
+ @staticmethod
160
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
161
+ parts = []
162
+ system_marker = f"\n{Llama3Template.format_system(system)}\n" if system else ""
163
+
164
+ for i, turn in enumerate(turns):
165
+ role = turn.get("role", "user")
166
+ content = turn.get("content", "")
167
+
168
+ if role == "user":
169
+ if i == 0 and system:
170
+ # First user turn gets the system prompt embedded
171
+ parts.append(f"[INST] {system_marker}{content} [/INST]")
172
+ else:
173
+ parts.append(f"[INST] {content} [/INST]")
174
+ elif role == "assistant":
175
+ parts.append(f" {content} ")
176
+
177
+ return "".join(parts)
178
+
179
+
180
+ class PhiTemplate(ABC):
181
+ """Phi models respond better to shorter, more direct prompts."""
182
+
183
+ family = ModelFamily.PHI
184
+ prefer_shorter_system_prompts = True
185
+ supports_xml_structured_output = False
186
+ supports_json_mode = False
187
+ max_recommended_system_tokens = 512
188
+
189
+ @staticmethod
190
+ def format_system(system_content: str) -> str:
191
+ return system_content
192
+
193
+ @staticmethod
194
+ def format_user(user_content: str) -> str:
195
+ return user_content
196
+
197
+ @staticmethod
198
+ def format_assistant(assistant_content: str) -> str:
199
+ return assistant_content
200
+
201
+ @staticmethod
202
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
203
+ parts = []
204
+ if system:
205
+ parts.append(system)
206
+ for turn in turns:
207
+ content = turn.get("content", "")
208
+ if content:
209
+ parts.append(content)
210
+ return "\n\n".join(parts)
211
+
212
+
213
+ class MistralTemplate(ABC):
214
+ """Mistral uses [INST] markers but places system prompt differently."""
215
+
216
+ family = ModelFamily.MISTRAL
217
+ prefer_shorter_system_prompts = False
218
+ supports_xml_structured_output = False
219
+ supports_json_mode = False
220
+ max_recommended_system_tokens = 2048
221
+
222
+ @staticmethod
223
+ def format_system(system_content: str) -> str:
224
+ return f"[SYSTEM] {system_content}"
225
+
226
+ @staticmethod
227
+ def format_user(user_content: str) -> str:
228
+ return f"[INST] {user_content} [/INST]"
229
+
230
+ @staticmethod
231
+ def format_assistant(assistant_content: str) -> str:
232
+ return assistant_content
233
+
234
+ @staticmethod
235
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
236
+ parts = []
237
+ if system:
238
+ parts.append(MistralTemplate.format_system(system))
239
+
240
+ for turn in turns:
241
+ role = turn.get("role", "user")
242
+ content = turn.get("content", "")
243
+
244
+ if role == "user":
245
+ parts.append(f"[INST] {content} [/INST]")
246
+ elif role == "assistant":
247
+ parts.append(f" {content}")
248
+
249
+ return " ".join(parts)
250
+
251
+
252
+ class GemmaTemplate(ABC):
253
+ """Gemma is similar to Llama3 with a direct message format."""
254
+
255
+ family = ModelFamily.GEMMA
256
+ prefer_shorter_system_prompts = False
257
+ supports_xml_structured_output = False
258
+ supports_json_mode = False
259
+ max_recommended_system_tokens = 2048
260
+
261
+ @staticmethod
262
+ def format_system(system_content: str) -> str:
263
+ return system_content
264
+
265
+ @staticmethod
266
+ def format_user(user_content: str) -> str:
267
+ return f"<start_of_turn>user\n{user_content}<end_of_turn>"
268
+
269
+ @staticmethod
270
+ def format_assistant(assistant_content: str) -> str:
271
+ return f"<start_of_turn>model\n{assistant_content}<end_of_turn>"
272
+
273
+ @staticmethod
274
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
275
+ parts = []
276
+ if system:
277
+ parts.append(f"<start_of_turn>user\n{system}<end_of_turn>")
278
+
279
+ for turn in turns:
280
+ role = turn.get("role", "user")
281
+ content = turn.get("content", "")
282
+
283
+ if role == "user":
284
+ parts.append(f"<start_of_turn>user\n{content}<end_of_turn>")
285
+ elif role == "assistant":
286
+ parts.append(f"<start_of_turn>model\n{content}<end_of_turn>")
287
+
288
+ parts.append("<start_of_turn>model\n")
289
+ return "".join(parts)
290
+
291
+
292
+ class StandardAPITemplate(ABC):
293
+ """Standard API format for Claude, GPT, Gemini, etc.
294
+
295
+ Uses message lists with role/content pairs.
296
+ System prompt is a separate message with role="system".
297
+ """
298
+
299
+ family = ModelFamily.UNKNOWN
300
+ prefer_shorter_system_prompts = False
301
+ supports_xml_structured_output = True
302
+ supports_json_mode = True
303
+ max_recommended_system_tokens = 4096
304
+
305
+ @staticmethod
306
+ def format_system(system_content: str) -> str:
307
+ return system_content
308
+
309
+ @staticmethod
310
+ def format_user(user_content: str) -> str:
311
+ return user_content
312
+
313
+ @staticmethod
314
+ def format_assistant(assistant_content: str) -> str:
315
+ return assistant_content
316
+
317
+ @staticmethod
318
+ def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
319
+ # For standard API, this just reconstructs the message list as text
320
+ # (mainly for debugging or conversion)
321
+ parts = []
322
+ if system:
323
+ parts.append(f"System: {system}")
324
+ for turn in turns:
325
+ role = turn.get("role", "unknown")
326
+ content = turn.get("content", "")
327
+ parts.append(f"{role.capitalize()}: {content}")
328
+ return "\n\n".join(parts)
329
+
330
+
331
+ # ─────────────────────────────────────────────────────────────────────────────
332
+ # Prompt Adaptation Engine
333
+ # ─────────────────────────────────────────────────────────────────────────────
334
+
335
+
336
+ class PromptAdaptationEngine:
337
+ """Adapts prompts and messages to match each model family's format.
338
+
339
+ Handles prompt reformatting, system prompt truncation, and message
340
+ list conversion for different model families.
341
+ """
342
+
343
+ def __init__(self) -> None:
344
+ """Initialize with template implementations for each family."""
345
+ self._templates = {
346
+ ModelFamily.QWEN: QwenTemplate,
347
+ ModelFamily.DEEPSEEK: DeepSeekTemplate,
348
+ ModelFamily.LLAMA3: Llama3Template,
349
+ ModelFamily.PHI: PhiTemplate,
350
+ ModelFamily.MISTRAL: MistralTemplate,
351
+ ModelFamily.GEMMA: GemmaTemplate,
352
+ ModelFamily.CLAUDE: StandardAPITemplate,
353
+ ModelFamily.GPT: StandardAPITemplate,
354
+ ModelFamily.GEMINI: StandardAPITemplate,
355
+ ModelFamily.UNKNOWN: StandardAPITemplate,
356
+ }
357
+
358
+ def get_template(self, model: ModelDescriptor) -> type:
359
+ """Get the prompt template for a model.
360
+
361
+ Parameters
362
+ ----------
363
+ model:
364
+ The model descriptor.
365
+
366
+ Returns
367
+ -------
368
+ type:
369
+ The template class for the model's family.
370
+ """
371
+ family = detect_family(model.model_id)
372
+ return self._templates.get(family, StandardAPITemplate)
373
+
374
+ def adapt_system_prompt(self, system: str, model: ModelDescriptor) -> str:
375
+ """Adapt and truncate a system prompt to match the model's preferences.
376
+
377
+ Parameters
378
+ ----------
379
+ system:
380
+ The original system prompt.
381
+ model:
382
+ The target model descriptor.
383
+
384
+ Returns
385
+ -------
386
+ str:
387
+ The adapted system prompt, truncated if needed.
388
+ """
389
+ if not system:
390
+ return ""
391
+
392
+ template = self.get_template(model)
393
+ max_tokens = template.max_recommended_system_tokens
394
+
395
+ # Check if truncation is needed
396
+ token_count = estimate_tokens(system)
397
+ if token_count > max_tokens:
398
+ # Truncate to fit within budget
399
+ target_char_count = max_tokens * 4 # Rough estimate
400
+ truncated = system[:target_char_count].rsplit(" ", 1)[0]
401
+ truncated += "\n[... truncated due to model constraints ...]"
402
+ logger.debug(
403
+ "System prompt truncated from %d to %d tokens for %s",
404
+ token_count,
405
+ estimate_tokens(truncated),
406
+ model.model_id,
407
+ )
408
+ return truncated
409
+
410
+ return system
411
+
412
+ def adapt_messages(
413
+ self, messages: list[dict[str, str]], model: ModelDescriptor
414
+ ) -> list[dict[str, str]]:
415
+ """Adapt message list to match the model's expected format.
416
+
417
+ For models with specialized formats (Qwen, DeepSeek, Llama3, etc.),
418
+ this converts the standard message list into the model's native format.
419
+
420
+ For cloud APIs (Claude, GPT, Gemini), the message list is returned
421
+ unchanged since they already use the standard format.
422
+
423
+ Parameters
424
+ ----------
425
+ messages:
426
+ Standard message list with 'role' and 'content' keys.
427
+ model:
428
+ The target model descriptor.
429
+
430
+ Returns
431
+ -------
432
+ list[dict[str, str]]:
433
+ Messages in the format expected by the model.
434
+ """
435
+ family = detect_family(model.model_id)
436
+
437
+ # Cloud APIs and unknown models use standard message format; return as-is
438
+ if family in (ModelFamily.CLAUDE, ModelFamily.GPT, ModelFamily.GEMINI, ModelFamily.UNKNOWN):
439
+ return messages
440
+
441
+ # For specialized formats, wrap in a single "text" message
442
+ # (Some local models expect a single text prompt)
443
+ template = self._templates[family]
444
+ system = ""
445
+ turns = []
446
+
447
+ for msg in messages:
448
+ role = msg.get("role", "user")
449
+ content = msg.get("content", "")
450
+
451
+ if role == "system":
452
+ system = content
453
+ else:
454
+ turns.append({"role": role, "content": content})
455
+
456
+ # Format as complete conversation text
457
+ formatted_text = template.format_full_conversation(system, turns)
458
+
459
+ return [{"role": "user", "content": formatted_text}]
460
+
461
+ def get_capabilities(self, model: ModelDescriptor) -> dict[str, bool | int]:
462
+ """Get formatting capabilities for a model.
463
+
464
+ Parameters
465
+ ----------
466
+ model:
467
+ The model descriptor.
468
+
469
+ Returns
470
+ -------
471
+ dict:
472
+ Capabilities including XML support, JSON mode, max system tokens.
473
+ """
474
+ template = self.get_template(model)
475
+ return {
476
+ "prefer_shorter_prompts": template.prefer_shorter_system_prompts,
477
+ "supports_xml": template.supports_xml_structured_output,
478
+ "supports_json_mode": template.supports_json_mode,
479
+ "max_system_tokens": template.max_recommended_system_tokens,
480
+ }
@@ -0,0 +1,99 @@
1
+ """Context section definitions and chunk metadata.
2
+
3
+ Defines the canonical sections that comprise the context window,
4
+ with explicit ordering and priority handling for trimming.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import IntEnum
11
+
12
+
13
+ class ContextSection(IntEnum):
14
+ """Canonical sections in the context window, ordered by priority.
15
+
16
+ Lower numbers = higher priority in the final assembled context.
17
+ Never trimmed: SYSTEM_PROMPT (always first), ARCHITECTURAL_DRIFT (urgent),
18
+ CURRENT_PROMPT (always last).
19
+ """
20
+
21
+ SYSTEM_PROMPT = 1 # System instructions and role definition
22
+ COGNITIVE_CONTINUITY = 2 # Lineage decisions, failed experiments, lessons learned
23
+ ARCHITECTURAL_DRIFT = 3 # URGENT: Current violations, breaking changes
24
+ REPOSITORY_SNAPSHOT = 4 # File structure, detected architecture, codebase state
25
+ RETRIEVED_CONTEXT = 5 # Results from RetrievalPipeline (most droppable)
26
+ WORKING_MEMORY = 6 # Current session turns, conversation history
27
+ CURRENT_PROMPT = 7 # The immediate user request (always last)
28
+
29
+
30
+ @dataclass
31
+ class ContextChunk:
32
+ """A chunk of assembled context with metadata for budget management.
33
+
34
+ Chunks are grouped by section and trimmed based on trust_score and priority
35
+ when the assembled context exceeds budget limits.
36
+ """
37
+
38
+ section: ContextSection
39
+ content: str
40
+ token_count: int
41
+ source: str # Debugging: where did this chunk come from?
42
+ trust_score: float = 1.0 # 0.0 to 1.0 (lower = more likely to trim)
43
+ priority: float = 0.5 # Within-section priority for trimming
44
+ metadata: dict = field(default_factory=dict)
45
+
46
+ def __post_init__(self) -> None:
47
+ """Validate chunk integrity."""
48
+ if not (0.0 <= self.trust_score <= 1.0):
49
+ raise ValueError(f"trust_score must be 0.0-1.0, got {self.trust_score}")
50
+ if not self.content or not isinstance(self.content, str):
51
+ raise ValueError("content must be a non-empty string")
52
+ if self.token_count < 0:
53
+ raise ValueError(f"token_count must be non-negative, got {self.token_count}")
54
+
55
+ def is_trimmable(self) -> bool:
56
+ """Check if this chunk can be trimmed when over budget.
57
+
58
+ NEVER trim: SYSTEM_PROMPT, ARCHITECTURAL_DRIFT, CURRENT_PROMPT.
59
+ """
60
+ return self.section not in (
61
+ ContextSection.SYSTEM_PROMPT,
62
+ ContextSection.ARCHITECTURAL_DRIFT,
63
+ ContextSection.CURRENT_PROMPT,
64
+ )
65
+
66
+ def trim_score(self) -> float:
67
+ """Score for sorting chunks by trim-ability (lower = trim first).
68
+
69
+ Lower scores are candidates for trimming. Combines trust_score
70
+ (lower = less trustworthy) and priority (lower = less important).
71
+ """
72
+ return self.trust_score * self.priority
73
+
74
+
75
+ @dataclass
76
+ class ContextAssemblyReport:
77
+ """Report of how context was assembled and trimmed."""
78
+
79
+ total_chunks_received: int
80
+ total_tokens_requested: int
81
+ total_tokens_assembled: int
82
+ sections_present: list[ContextSection] = field(default_factory=list)
83
+ sections_trimmed: dict[ContextSection, int] = field(default_factory=dict)
84
+ chunks_dropped: int = 0
85
+ budget_exceeded: bool = False
86
+
87
+ def __str__(self) -> str:
88
+ """Human-readable assembly report."""
89
+ lines = [
90
+ "ContextAssemblyReport:",
91
+ f" Chunks: {self.total_chunks_received} received, {self.chunks_dropped} dropped",
92
+ f" Tokens: {self.total_tokens_assembled}/{self.total_tokens_requested}",
93
+ f" Sections: {len(self.sections_present)} present",
94
+ ]
95
+ if self.sections_trimmed:
96
+ lines.append(f" Trimmed: {self.sections_trimmed}")
97
+ if self.budget_exceeded:
98
+ lines.append(" WARNING: Budget exceeded even after trimming")
99
+ return "\n".join(lines)