@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,208 @@
1
+ """Provider-neutral context setup projection and mutation service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .architecture import ProviderContextPolicy
10
+
11
+
12
+ ContextMode = tuple[int, int, int]
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ContextSetupPorts:
17
+ context_capacity: Callable[[str, dict[str, Any]], int | None]
18
+ context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
19
+ positive_int: Callable[[Any], int | None]
20
+ format_context: Callable[[int | None], str]
21
+ ui_text: Callable[[str, str], str]
22
+ pad_cells: Callable[[str, int], str]
23
+ cap_context: Callable[[str, dict[str, Any]], list[str]]
24
+ cap_output: Callable[[str, dict[str, Any]], list[str]]
25
+ apply_timeout: Callable[[str, dict[str, Any]], list[str]]
26
+ context_status: Callable[[str, dict[str, Any]], str]
27
+
28
+
29
+ _ORDERED_MODES = (
30
+ "context-compact",
31
+ "context-balanced",
32
+ "context-project",
33
+ "context-full",
34
+ )
35
+ _TEXT = {
36
+ "en": {
37
+ "context-compact": ("Compact / fast", "small context, faster and cheaper"),
38
+ "context-balanced": (
39
+ "Balanced",
40
+ "good default for normal coding sessions",
41
+ ),
42
+ "context-project": (
43
+ "Large project",
44
+ "more files/history, slower but safer for big work",
45
+ ),
46
+ "context-full": (
47
+ "Full model window",
48
+ "use the selected model's maximum context",
49
+ ),
50
+ },
51
+ "ko": {
52
+ "context-compact": ("컴팩트/빠름", "작은 컨텍스트, 빠르고 가벼움"),
53
+ "context-balanced": ("균형형", "일반 코딩 세션의 권장 기본값"),
54
+ "context-project": (
55
+ "대형 프로젝트",
56
+ "파일/히스토리를 더 많이 사용, 큰 작업에 안정적",
57
+ ),
58
+ "context-full": (
59
+ "모델 최대 컨텍스트",
60
+ "선택한 모델의 최대 컨텍스트 사용",
61
+ ),
62
+ },
63
+ "ja": {
64
+ "context-compact": (
65
+ "コンパクト/高速",
66
+ "小さなコンテキストで高速かつ軽量",
67
+ ),
68
+ "context-balanced": (
69
+ "バランス",
70
+ "通常のコーディングセッション向けの既定値",
71
+ ),
72
+ "context-project": (
73
+ "大規模プロジェクト",
74
+ "より多くのファイル/履歴を使う大型作業向け",
75
+ ),
76
+ "context-full": (
77
+ "モデル最大コンテキスト",
78
+ "選択モデルの最大コンテキストを使用",
79
+ ),
80
+ },
81
+ "zh": {
82
+ "context-compact": ("紧凑/快速", "较小上下文,更快更轻"),
83
+ "context-balanced": ("均衡", "普通编码会话的推荐默认值"),
84
+ "context-project": ("大型项目", "使用更多文件/历史,适合大任务"),
85
+ "context-full": ("模型最大上下文", "使用所选模型的最大上下文"),
86
+ },
87
+ }
88
+
89
+
90
+ class ContextSetupService:
91
+ def __init__(self, ports: ContextSetupPorts) -> None:
92
+ self.ports = ports
93
+
94
+ @staticmethod
95
+ def mode_values(capacity: int | None) -> dict[str, ContextMode]:
96
+ cap = capacity or 131072
97
+
98
+ def clamp(value: int) -> int:
99
+ return max(8192, min(cap, value))
100
+
101
+ compact = clamp(32768)
102
+ balanced = clamp(65536 if cap <= 131072 else 131072)
103
+ project = clamp(262144 if cap >= 262144 else cap)
104
+ full = clamp(cap)
105
+ return {
106
+ "context-compact": (
107
+ compact,
108
+ min(2048, max(1024, compact // 16)),
109
+ 4096,
110
+ ),
111
+ "context-balanced": (
112
+ balanced,
113
+ min(4096, max(2048, balanced // 16)),
114
+ 4096,
115
+ ),
116
+ "context-project": (
117
+ project,
118
+ min(8192, max(4096, project // 16)),
119
+ 8192,
120
+ ),
121
+ "context-full": (
122
+ full,
123
+ min(16384, max(4096, full // 16)),
124
+ 8192,
125
+ ),
126
+ }
127
+
128
+ @staticmethod
129
+ def text(key: str, language: str) -> tuple[str, str]:
130
+ entries = _TEXT.get(language, _TEXT["en"])
131
+ return entries.get(key, _TEXT["en"][key])
132
+
133
+ def panel_rows(
134
+ self,
135
+ provider: str,
136
+ config: dict[str, Any],
137
+ language: str,
138
+ ) -> tuple[list[str], list[str]]:
139
+ capacity = self.ports.context_capacity(provider, config)
140
+ rows = [f"Model context capacity: {self.ports.format_context(capacity)}"]
141
+ values = ["__info__"]
142
+ strategy = self.ports.context_policy(provider, config).settings_strategy
143
+ if strategy == "managed":
144
+ rows.append("Claude Code manages Anthropic context automatically.")
145
+ values.append("__info__")
146
+ rows.append(self.ports.ui_text("back", language))
147
+ values.append("back")
148
+ return rows, values
149
+ current_window = self.ports.positive_int(
150
+ config.get("num_ctx_max" if strategy == "ollama" else "context_window")
151
+ )
152
+ choices = self.mode_values(capacity)
153
+ visible_modes: list[str] = []
154
+ seen_windows: set[int] = set()
155
+ for key in reversed(_ORDERED_MODES):
156
+ window = choices[key][0]
157
+ if window not in seen_windows:
158
+ seen_windows.add(window)
159
+ visible_modes.append(key)
160
+ for key in reversed(visible_modes):
161
+ window, _reserve, output = choices[key]
162
+ label, description = self.text(key, language)
163
+ mark = "*" if current_window == window else " "
164
+ rows.append(
165
+ f"{mark} {self.ports.pad_cells(label, 22)} "
166
+ f"{self.ports.format_context(window):>6} "
167
+ f"out {self.ports.format_context(output):>5} {description}"
168
+ )
169
+ values.append(key)
170
+ rows.append(self.ports.ui_text("back", language))
171
+ values.append("back")
172
+ return rows, values
173
+
174
+ def apply(
175
+ self,
176
+ provider: str,
177
+ config: dict[str, Any],
178
+ mode: str,
179
+ language: str,
180
+ ) -> list[str]:
181
+ choices = self.mode_values(self.ports.context_capacity(provider, config))
182
+ if mode not in choices:
183
+ raise SystemExit(f"Unknown context mode: {mode}")
184
+ window, reserve, output = choices[mode]
185
+ label = self.text(mode, language)[0]
186
+ strategy = self.ports.context_policy(provider, config).settings_strategy
187
+ if strategy == "ollama":
188
+ config["num_ctx"] = "auto"
189
+ config["num_ctx_max"] = window
190
+ config["num_ctx_min"] = min(
191
+ window,
192
+ 32768 if window <= 65536 else 65536,
193
+ )
194
+ config.setdefault("ollama_options", {})["num_predict"] = output
195
+ elif strategy == "standard":
196
+ config["context_window"] = window
197
+ config["context_reserve_tokens"] = reserve
198
+ config["max_output_tokens"] = output
199
+ else:
200
+ return ["Context setup is managed by Claude Code for this provider."]
201
+ messages = self.ports.cap_context(provider, config)
202
+ messages.extend(self.ports.cap_output(provider, config))
203
+ messages.extend(self.ports.apply_timeout(provider, config))
204
+ return [
205
+ f"{self.ports.ui_text('context_setup', language)}: {label}",
206
+ f"Applied context: {self.ports.context_status(provider, config)}",
207
+ *messages,
208
+ ]
@@ -0,0 +1,392 @@
1
+ """Pure message projection and chunking policy for context compaction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable
8
+
9
+
10
+ PROMPT_TOOL_INPUT_FIELD_LIMIT = 1200
11
+ PROMPT_MESSAGE_TEXT_LIMIT = 20000
12
+ CLAUDE_CODE_PERSISTED_OUTPUT_MARKER = "<persisted-output>"
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ContextSummaryPolicy:
17
+ estimate_tokens: Callable[[Any], int]
18
+ positive_int: Callable[[Any], int | None]
19
+ content_to_text: Callable[[Any], str]
20
+ compact_json: Callable[..., str]
21
+ latest_user_text: Callable[[dict[str, Any]], str]
22
+
23
+ def is_compact_request(self, body: dict[str, Any]) -> bool:
24
+ if not isinstance(body, dict):
25
+ return False
26
+ text = self.latest_user_text(body).lower()
27
+ return bool(
28
+ text
29
+ and (
30
+ "<command-name>/compact</command-name>" in text
31
+ or ("<command-message>compact</command-message>" in text and "<command-name>" in text)
32
+ or ("create a detailed summary of the conversation" in text and "compact" in text)
33
+ or ("summarize the conversation so far" in text and "compact" in text)
34
+ )
35
+ )
36
+
37
+ def text_only_body(
38
+ self,
39
+ body: dict[str, Any],
40
+ system_prompt: str,
41
+ append_system: Callable[[Any, list[str]], Any],
42
+ log: Callable[[str, str], None],
43
+ ) -> dict[str, Any]:
44
+ if not self.is_compact_request(body):
45
+ return body
46
+ result = dict(body)
47
+ removed_tools = bool(result.pop("tools", None))
48
+ removed_choice = bool(result.pop("tool_choice", None))
49
+ result.pop("parallel_tool_calls", None)
50
+ result["system"] = append_system(result.get("system"), [system_prompt])
51
+ if removed_tools or removed_choice:
52
+ log(
53
+ "INFO",
54
+ "compact_request_text_only removed_tools=%s removed_tool_choice=%s"
55
+ % (str(removed_tools).lower(), str(removed_choice).lower()),
56
+ )
57
+ return result
58
+
59
+ @staticmethod
60
+ def truncate(text: str, limit: int) -> str:
61
+ if len(text) <= limit:
62
+ return text
63
+ return text[:limit] + f"\n...[truncated {len(text) - limit} chars]..."
64
+
65
+ @staticmethod
66
+ def is_persisted_output(text: str) -> bool:
67
+ return CLAUDE_CODE_PERSISTED_OUTPUT_MARKER in str(text or "")
68
+
69
+ def compact_tool_value(self, value: Any, limit: int = PROMPT_TOOL_INPUT_FIELD_LIMIT) -> Any:
70
+ if isinstance(value, str):
71
+ return self.truncate(value, limit)
72
+ if isinstance(value, list):
73
+ return [self.compact_tool_value(item, limit) for item in value[:20]]
74
+ if isinstance(value, dict):
75
+ return {
76
+ key: self.truncate(item, limit)
77
+ if key in {"content", "old_string", "new_string", "command"}
78
+ and isinstance(item, str)
79
+ else self.compact_tool_value(item, limit)
80
+ for key, item in value.items()
81
+ }
82
+ return value
83
+
84
+ def tool_input(self, tool_input: Any) -> str:
85
+ if not tool_input:
86
+ return "{}"
87
+ return json.dumps(self.compact_tool_value(tool_input), ensure_ascii=False, sort_keys=True)
88
+
89
+ def message_text(self, text: str) -> str:
90
+ return self.truncate(text, PROMPT_MESSAGE_TEXT_LIMIT)
91
+
92
+ @staticmethod
93
+ def tool_markers(message: dict[str, Any]) -> list[str]:
94
+ content = message.get("content")
95
+ if not isinstance(content, list):
96
+ return []
97
+ markers: list[str] = []
98
+ for block in content:
99
+ if not isinstance(block, dict):
100
+ continue
101
+ if block.get("type") == "tool_use":
102
+ name = str(block.get("name") or "tool")
103
+ tool_id = str(block.get("id") or "")
104
+ markers.append(f"tool_use:{name}{('/' + tool_id) if tool_id else ''}")
105
+ elif block.get("type") == "tool_result":
106
+ markers.append(f"tool_result:{str(block.get('tool_use_id') or 'tool')}")
107
+ return markers
108
+
109
+ def summary_line(self, index: int, message: dict[str, Any], text_limit: int = 700) -> str:
110
+ role = str(message.get("role") or "unknown")
111
+ text = " ".join(self.content_to_text(message.get("content")).split())
112
+ parts = [f"message {index}", f"role={role}"]
113
+ if markers := self.tool_markers(message):
114
+ parts.append("markers=" + ",".join(markers[:6]))
115
+ if text:
116
+ parts.append("text=" + self.truncate(text, text_limit))
117
+ return "- " + " | ".join(parts)
118
+
119
+ @staticmethod
120
+ def chunk_ranges(count: int, chunks: int) -> list[tuple[int, int]]:
121
+ chunks = max(1, min(chunks, count))
122
+ return [
123
+ (start, end)
124
+ for index in range(chunks)
125
+ if (start := (index * count) // chunks) < (end := ((index + 1) * count) // chunks)
126
+ ]
127
+
128
+ def guard_chunk_count(
129
+ self,
130
+ omitted_messages: list[dict[str, Any]],
131
+ budget_tokens: int | None = None,
132
+ ) -> int:
133
+ if not omitted_messages:
134
+ return 0
135
+ omitted_tokens = sum(self.estimate_tokens(message) for message in omitted_messages)
136
+ target = 32768
137
+ if budget := self.positive_int(budget_tokens):
138
+ target = max(target, min(262144, max(1, budget // 4)))
139
+ return max(1, min(12, (omitted_tokens + target - 1) // target))
140
+
141
+ def guard_summary(
142
+ self,
143
+ omitted_messages: list[dict[str, Any]],
144
+ budget_tokens: int,
145
+ start_index: int = 0,
146
+ ) -> str:
147
+ count = len(omitted_messages)
148
+ tokens = sum(self.estimate_tokens(message) for message in omitted_messages)
149
+ if count <= 0:
150
+ return (
151
+ "[ciel-runtime context guard: older conversation history was compacted because "
152
+ f"the provider context budget is {budget_tokens} tokens.]"
153
+ )
154
+ max_tokens = max(1024, min(24576, max(1, budget_tokens) // 10))
155
+ max_chars = max_tokens * 4
156
+ chunks = self.guard_chunk_count(omitted_messages, budget_tokens)
157
+ lines = [
158
+ f"[ciel-runtime context guard: compacted {count} older messages, approx {tokens} tokens, because the provider context budget is {budget_tokens} tokens.]",
159
+ "The recent tail is preserved verbatim. Older history is represented below as deterministic chunk summaries; use file reads or MCP queries if exact old content is needed.",
160
+ ]
161
+ for number, (start, end) in enumerate(self.chunk_ranges(count, chunks), start=1):
162
+ chunk = omitted_messages[start:end]
163
+ chunk_tokens = sum(self.estimate_tokens(message) for message in chunk)
164
+ lines.append(
165
+ f"Chunk {number}/{chunks}: messages {start_index + start}-{start_index + end - 1}, approx {chunk_tokens} tokens."
166
+ )
167
+ offsets = list(range(len(chunk))) if len(chunk) <= 4 else [0, 1, len(chunk) - 2, len(chunk) - 1]
168
+ for offset in dict.fromkeys(offsets):
169
+ lines.append(self.summary_line(start_index + start + offset, chunk[offset]))
170
+ if len("\n".join(lines)) > max_chars:
171
+ lines.append(f"...[context guard summary truncated to {max_tokens} tokens]...")
172
+ break
173
+ summary = "\n".join(lines)
174
+ return self.truncate(summary, max_chars) if len(summary) > max_chars else summary
175
+
176
+ def compact_message(self, message: dict[str, Any], index: int) -> str:
177
+ role = str(message.get("role") or "unknown")
178
+ parts = [f"Message {index} role={role}"]
179
+ if name := message.get("name") or message.get("tool_name"):
180
+ parts.append(f"name={name}")
181
+ if message.get("tool_call_id"):
182
+ parts.append(f"tool_call_id={message.get('tool_call_id')}")
183
+ content = self.content_to_text(message.get("content"))
184
+ if message.get("tool_calls"):
185
+ tool_calls = "tool_calls=" + self.compact_json(message.get("tool_calls"), max_chars=6000)
186
+ content = f"{content}\n\n{tool_calls}" if content else tool_calls
187
+ return f"{' '.join(parts)}\n{self.message_text(content)}"
188
+
189
+ def instruction_index(self, messages: list[dict[str, Any]]) -> int | None:
190
+ fallback: int | None = None
191
+ for index, message in enumerate(messages):
192
+ if str(message.get("role") or "") != "user":
193
+ continue
194
+ text = self.content_to_text(message.get("content")).lower()
195
+ if text:
196
+ fallback = index
197
+ if (
198
+ "<command-name>/compact</command-name>" in text
199
+ or ("<command-message>compact</command-message>" in text and "<command-name>" in text)
200
+ or ("create a detailed summary of the conversation" in text and "compact" in text)
201
+ or ("summarize the conversation so far" in text and "compact" in text)
202
+ ):
203
+ return index
204
+ return fallback
205
+
206
+ def chunk_target_tokens(self, config: dict[str, Any] | None, budget_tokens: int) -> int:
207
+ configured = self.positive_int((config or {}).get("context_compact_chunk_tokens"))
208
+ return max(8192, configured) if configured else max(8192, min(65536, max(1, budget_tokens) // 4))
209
+
210
+ def summary_output_tokens(self, config: dict[str, Any] | None, budget_tokens: int) -> int:
211
+ configured = self.positive_int((config or {}).get("context_compact_summary_tokens"))
212
+ return max(512, configured) if configured else max(1024, min(8192, max(1, budget_tokens) // 64))
213
+
214
+ def split_messages(
215
+ self,
216
+ messages: list[dict[str, Any]],
217
+ target_tokens: int,
218
+ ) -> list[tuple[int, list[dict[str, Any]]]]:
219
+ chunks: list[tuple[int, list[dict[str, Any]]]] = []
220
+ current: list[dict[str, Any]] = []
221
+ current_start = 0
222
+ current_tokens = 0
223
+ for index, message in enumerate(messages):
224
+ tokens = max(1, self.estimate_tokens(message))
225
+ if current and current_tokens + tokens > target_tokens:
226
+ chunks.append((current_start, current))
227
+ current, current_tokens, current_start = [], 0, index
228
+ if not current:
229
+ current_start = index
230
+ current.append(message)
231
+ current_tokens += tokens
232
+ if current:
233
+ chunks.append((current_start, current))
234
+ return chunks
235
+
236
+ def chunk_prompt(
237
+ self,
238
+ chunk: list[dict[str, Any]],
239
+ start_index: int,
240
+ chunk_number: int,
241
+ chunk_total: int,
242
+ ) -> str:
243
+ parts = [
244
+ f"Segment {chunk_number}/{chunk_total}. Summarize messages {start_index}-{start_index + len(chunk) - 1}.",
245
+ "Return only the segment summary.",
246
+ ]
247
+ parts.extend(
248
+ self.compact_message(message, start_index + offset)
249
+ for offset, message in enumerate(chunk)
250
+ )
251
+ return "\n\n".join(parts)
252
+
253
+ def extract_response_text(self, data: Any, wire: str) -> str:
254
+ if not isinstance(data, dict):
255
+ return ""
256
+ if wire == "ollama":
257
+ message = data.get("message") if isinstance(data.get("message"), dict) else {}
258
+ return str(message.get("content") or data.get("response") or "").strip()
259
+ if wire == "openai":
260
+ choices = data.get("choices")
261
+ if isinstance(choices, list) and choices:
262
+ choice = choices[0] if isinstance(choices[0], dict) else {}
263
+ message = choice.get("message") if isinstance(choice.get("message"), dict) else {}
264
+ return str(message.get("content") or "").strip()
265
+ return ""
266
+ if wire == "anthropic":
267
+ return self.content_to_text(data.get("content")).strip()
268
+ return ""
269
+
270
+ def reduce_prompt(
271
+ self,
272
+ summaries: list[str],
273
+ compact_instruction: str,
274
+ budget_tokens: int,
275
+ source_message_count: int,
276
+ ) -> str:
277
+ parts = [
278
+ "[ciel-runtime segmented compact]",
279
+ f"The previous conversation was too large for a single compact request. It was summarized in {len(summaries)} segment(s) from {source_message_count} message(s).",
280
+ "Segment summaries:",
281
+ ]
282
+ parts.extend(
283
+ f"## Segment {index}\n{summary.strip()}"
284
+ for index, summary in enumerate(summaries, start=1)
285
+ )
286
+ parts.extend(
287
+ (
288
+ "Claude Code compact instruction:",
289
+ self.message_text(compact_instruction),
290
+ "Using the segment summaries above, return only the final compact summary text requested by Claude Code.",
291
+ )
292
+ )
293
+ text = "\n\n".join(parts)
294
+ max_chars = max(8192, max(1, budget_tokens) * 3)
295
+ return self.truncate(text, max_chars) if len(text) > max_chars else text
296
+
297
+
298
+ @dataclass(frozen=True, slots=True)
299
+ class ContextSummaryCompatibilityApi:
300
+ """Typed facade adapter for context-summary projections."""
301
+
302
+ policy_factory: Callable[[], ContextSummaryPolicy]
303
+ compact_system_prompt: str
304
+ append_system: Callable[[Any, list[str]], Any]
305
+ log: Callable[[str, str], None]
306
+
307
+ def is_compact_request(self, body: dict[str, Any]) -> bool:
308
+ return self.policy_factory().is_compact_request(body)
309
+
310
+ def text_only_body(self, body: dict[str, Any]) -> dict[str, Any]:
311
+ return self.policy_factory().text_only_body(
312
+ body, self.compact_system_prompt, self.append_system, self.log
313
+ )
314
+
315
+ def compact_tool_value(self, value: Any, limit: int = PROMPT_TOOL_INPUT_FIELD_LIMIT) -> Any:
316
+ return self.policy_factory().compact_tool_value(value, limit)
317
+
318
+ def tool_input(self, tool_input: Any) -> str:
319
+ return self.policy_factory().tool_input(tool_input)
320
+
321
+ def message_text(self, text: str) -> str:
322
+ return self.policy_factory().message_text(text)
323
+
324
+ def summary_line(
325
+ self, index: int, message: dict[str, Any], *, text_limit: int = 700
326
+ ) -> str:
327
+ return self.policy_factory().summary_line(index, message, text_limit)
328
+
329
+ def guard_chunk_count(
330
+ self,
331
+ omitted_messages: list[dict[str, Any]],
332
+ budget_tokens: int | None = None,
333
+ ) -> int:
334
+ return self.policy_factory().guard_chunk_count(omitted_messages, budget_tokens)
335
+
336
+ def guard_summary(
337
+ self,
338
+ omitted_messages: list[dict[str, Any]],
339
+ budget_tokens: int,
340
+ *,
341
+ start_index: int = 0,
342
+ ) -> str:
343
+ return self.policy_factory().guard_summary(
344
+ omitted_messages, budget_tokens, start_index
345
+ )
346
+
347
+ def compact_message(self, message: dict[str, Any], index: int) -> str:
348
+ return self.policy_factory().compact_message(message, index)
349
+
350
+ def instruction_index(self, messages: list[dict[str, Any]]) -> int | None:
351
+ return self.policy_factory().instruction_index(messages)
352
+
353
+ def chunk_target_tokens(
354
+ self, config: dict[str, Any] | None, budget_tokens: int
355
+ ) -> int:
356
+ return self.policy_factory().chunk_target_tokens(config, budget_tokens)
357
+
358
+ def summary_output_tokens(
359
+ self, config: dict[str, Any] | None, budget_tokens: int
360
+ ) -> int:
361
+ return self.policy_factory().summary_output_tokens(config, budget_tokens)
362
+
363
+ def split_messages(
364
+ self, messages: list[dict[str, Any]], target_tokens: int
365
+ ) -> list[tuple[int, list[dict[str, Any]]]]:
366
+ return self.policy_factory().split_messages(messages, target_tokens)
367
+
368
+ def chunk_prompt(
369
+ self,
370
+ chunk: list[dict[str, Any]],
371
+ start_index: int,
372
+ chunk_no: int,
373
+ chunk_total: int,
374
+ ) -> str:
375
+ return self.policy_factory().chunk_prompt(
376
+ chunk, start_index, chunk_no, chunk_total
377
+ )
378
+
379
+ def extract_response_text(self, data: Any, wire: str) -> str:
380
+ return self.policy_factory().extract_response_text(data, wire)
381
+
382
+ def reduce_prompt(
383
+ self,
384
+ summaries: list[str],
385
+ compact_instruction: str,
386
+ *,
387
+ budget_tokens: int,
388
+ source_message_count: int,
389
+ ) -> str:
390
+ return self.policy_factory().reduce_prompt(
391
+ summaries, compact_instruction, budget_tokens, source_message_count
392
+ )