@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,322 @@
1
+ """Protocol-aware prompt compaction policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class PromptCompactionText:
11
+ content_to_text: Callable[..., str]
12
+ compact_text: Callable[..., str]
13
+ build_summary: Callable[..., str]
14
+ append_system_texts: Callable[..., Any]
15
+ truncate: Callable[..., str]
16
+ chunk_count: Callable[..., int]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class PromptCompactionRuntime:
21
+ estimate_tokens: Callable[..., int]
22
+ llm_compact_messages: Callable[..., list[dict[str, Any]] | None]
23
+ write_activity: Callable[..., Any]
24
+ log: Callable[[str, str], Any]
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class PromptCompactionServices:
29
+ text: PromptCompactionText
30
+ runtime: PromptCompactionRuntime
31
+
32
+
33
+ def compact_chat_messages_for_budget(
34
+ messages: list[dict[str, Any]],
35
+ tools: list[dict[str, Any]],
36
+ budget_tokens: int,
37
+ *,
38
+ provider: str = "",
39
+ model: str = "",
40
+ pcfg: dict[str, Any] | None = None,
41
+ full_compact_request: bool = False,
42
+ wire: str | None = None,
43
+ services: PromptCompactionServices,
44
+ ) -> list[dict[str, Any]]:
45
+ if not messages:
46
+ return messages
47
+ text = services.text
48
+ runtime = services.runtime
49
+ budget_tokens = max(8192, budget_tokens)
50
+ initial_tokens = runtime.estimate_tokens({"messages": messages, "tools": tools})
51
+ if initial_tokens <= budget_tokens:
52
+ return messages
53
+ if full_compact_request and pcfg is not None and provider and model:
54
+ compacted_by_llm = runtime.llm_compact_messages(
55
+ provider,
56
+ model,
57
+ pcfg,
58
+ messages,
59
+ budget_tokens,
60
+ wire=wire or "ollama",
61
+ )
62
+ if compacted_by_llm:
63
+ final_tokens = runtime.estimate_tokens({"messages": compacted_by_llm, "tools": []})
64
+ if final_tokens <= budget_tokens:
65
+ return compacted_by_llm
66
+ runtime.log(
67
+ "WARN",
68
+ f"context_compact_map_reduce_oversize provider={provider} model={model} tokens={final_tokens} budget={budget_tokens}; falling back to deterministic compact",
69
+ )
70
+
71
+ def compact_message(message: dict[str, Any], max_chars: int, prefix: str) -> dict[str, Any]:
72
+ role = str(message.get("role") or "user")
73
+ if role not in ("system", "user", "assistant", "tool"):
74
+ role = "user"
75
+ content = message.get("content")
76
+ content_text = content if isinstance(content, str) else text.content_to_text(content)
77
+ out: dict[str, Any] = {"role": role}
78
+ if message.get("name"):
79
+ out["name"] = str(message.get("name"))
80
+ out["content"] = text.truncate(f"{prefix}\n{content_text}", max(256, max_chars))
81
+ return out
82
+
83
+ def hard_cap(
84
+ system_messages: list[dict[str, Any]],
85
+ latest_message: dict[str, Any] | None,
86
+ omitted_messages: list[dict[str, Any]],
87
+ ) -> list[dict[str, Any]]:
88
+ summary_text = text.build_summary(omitted_messages, max(8192, budget_tokens // 2))
89
+ summary_prefix = (
90
+ "[ciel-runtime context guard: older conversation history and oversized retained messages were "
91
+ "compacted because they exceeded the provider context budget.]"
92
+ )
93
+ latest_prefix = "[Latest retained message compacted because the previous payload exceeded the provider context budget.]"
94
+ system_chars = max(512, min(8192, budget_tokens))
95
+ summary_chars = max(512, min(4096, budget_tokens))
96
+ latest_chars = max(1024, budget_tokens * 4)
97
+ last_candidate: list[dict[str, Any]] = []
98
+ for _ in range(10):
99
+ per_system_chars = max(256, system_chars // max(1, len(system_messages)))
100
+ capped_systems = [
101
+ compact_message(message, per_system_chars, "[System message compacted for context budget.]")
102
+ for message in system_messages
103
+ ]
104
+ summary_message = {
105
+ "role": "user",
106
+ "content": text.truncate(f"{summary_prefix}\n{summary_text}", summary_chars),
107
+ }
108
+ candidate = [*capped_systems, summary_message]
109
+ base_tokens = runtime.estimate_tokens({"messages": candidate, "tools": tools})
110
+ remaining_chars = max(256, (budget_tokens - base_tokens) * 4 - 1024)
111
+ if latest_message is not None:
112
+ candidate.append(compact_message(latest_message, min(latest_chars, remaining_chars), latest_prefix))
113
+ last_candidate = candidate
114
+ if runtime.estimate_tokens({"messages": candidate, "tools": tools}) <= budget_tokens:
115
+ return candidate
116
+ system_chars = max(256, system_chars // 2)
117
+ summary_chars = max(256, summary_chars // 2)
118
+ latest_chars = max(256, latest_chars // 2)
119
+ return last_candidate
120
+
121
+ system_messages = [message for message in messages if message.get("role") == "system"]
122
+ non_system = [message for message in messages if message.get("role") != "system"]
123
+ first_user = next((message for message in non_system if message.get("role") == "user"), None)
124
+ preserved_tail: list[dict[str, Any]] = []
125
+ omitted_reversed: list[dict[str, Any]] = []
126
+ summary: dict[str, Any] = {
127
+ "role": "user",
128
+ "content": (
129
+ "[ciel-runtime context guard: older conversation messages were omitted because the provider context "
130
+ "budget would be exceeded. Large file contents and prior Write/Edit inputs were truncated. "
131
+ "Use Read on specific files if exact old content is needed.]"
132
+ ),
133
+ }
134
+ fixed_prefix = list(system_messages)
135
+ if first_user is not None:
136
+ fixed_prefix.append(first_user)
137
+ fixed_prefix.append(summary)
138
+ for message in reversed(non_system):
139
+ if first_user is not None and message is first_user:
140
+ continue
141
+ candidate = fixed_prefix + list(reversed(preserved_tail + [message]))
142
+ if runtime.estimate_tokens({"messages": candidate, "tools": tools}) <= budget_tokens:
143
+ preserved_tail.append(message)
144
+ else:
145
+ omitted_reversed.append(message)
146
+ if first_user is None:
147
+ fixed_prefix = [*system_messages, summary]
148
+ omitted_messages = list(reversed(omitted_reversed))
149
+ summary["content"] = text.build_summary(omitted_messages, budget_tokens)
150
+ compacted = fixed_prefix + list(reversed(preserved_tail))
151
+ while runtime.estimate_tokens({"messages": compacted, "tools": tools}) > budget_tokens and preserved_tail:
152
+ omitted_messages.append(preserved_tail.pop())
153
+ summary["content"] = text.build_summary(omitted_messages, budget_tokens)
154
+ compacted = fixed_prefix + list(reversed(preserved_tail))
155
+ if runtime.estimate_tokens({"messages": compacted, "tools": tools}) > budget_tokens:
156
+ latest_message = next((message for message in reversed(non_system) if isinstance(message, dict)), None)
157
+ omitted_for_hard_cap = [
158
+ message
159
+ for message in messages
160
+ if message not in system_messages and (latest_message is None or message is not latest_message)
161
+ ]
162
+ compacted = hard_cap(system_messages, latest_message, omitted_for_hard_cap)
163
+ final_tokens = runtime.estimate_tokens({"messages": compacted, "tools": tools})
164
+ runtime.log(
165
+ "WARN",
166
+ f"compacted ollama payload messages {len(messages)}->{len(compacted)} tokens {initial_tokens}->{final_tokens} budget={budget_tokens}",
167
+ )
168
+ chunk_count = text.chunk_count(omitted_messages, budget_tokens)
169
+ if chunk_count and (provider or model):
170
+ runtime.write_activity(
171
+ provider or "provider",
172
+ model,
173
+ chunks=chunk_count,
174
+ parallel_sessions=1,
175
+ tokens=initial_tokens,
176
+ final_tokens=final_tokens,
177
+ budget=budget_tokens,
178
+ omitted_messages=len(omitted_messages),
179
+ retained_messages=len(compacted),
180
+ )
181
+ return compacted
182
+
183
+
184
+ def anthropic_message_has_tool_result(message: dict[str, Any]) -> bool:
185
+ content = message.get("content")
186
+ return isinstance(content, list) and any(
187
+ isinstance(block, dict) and block.get("type") == "tool_result"
188
+ for block in content
189
+ )
190
+
191
+
192
+ def anthropic_safe_tail_start(message: dict[str, Any]) -> bool:
193
+ return str(message.get("role") or "") == "user" and not anthropic_message_has_tool_result(message)
194
+
195
+
196
+ def compact_anthropic_body_for_budget(
197
+ body: dict[str, Any],
198
+ budget_tokens: int,
199
+ *,
200
+ provider: str = "",
201
+ model: str = "",
202
+ pcfg: dict[str, Any] | None = None,
203
+ full_compact_request: bool = False,
204
+ services: PromptCompactionServices,
205
+ ) -> dict[str, Any]:
206
+ messages = body.get("messages")
207
+ if not isinstance(messages, list) or not messages:
208
+ return body
209
+ typed_messages = [message for message in messages if isinstance(message, dict)]
210
+ if len(typed_messages) != len(messages):
211
+ return body
212
+ text = services.text
213
+ runtime = services.runtime
214
+ budget_tokens = max(8192, budget_tokens)
215
+ initial_tokens = runtime.estimate_tokens(body)
216
+ if initial_tokens <= budget_tokens:
217
+ return body
218
+ if full_compact_request and pcfg is not None and provider and model:
219
+ compact_messages = runtime.llm_compact_messages(
220
+ provider,
221
+ model,
222
+ pcfg,
223
+ typed_messages,
224
+ budget_tokens,
225
+ wire="anthropic",
226
+ )
227
+ if compact_messages:
228
+ compacted = dict(body)
229
+ compacted["messages"] = compact_messages
230
+ compacted["tools"] = []
231
+ compacted.pop("tool_choice", None)
232
+ compacted.pop("parallel_tool_calls", None)
233
+ final_tokens = runtime.estimate_tokens(compacted)
234
+ if final_tokens <= budget_tokens:
235
+ return compacted
236
+ runtime.log(
237
+ "WARN",
238
+ f"context_compact_map_reduce_oversize provider={provider} model={model} tokens={final_tokens} budget={budget_tokens}; falling back to deterministic compact",
239
+ )
240
+
241
+ summary_budget = max(1024, min(24576, budget_tokens // 10))
242
+ tail_budget = max(8192, budget_tokens - summary_budget)
243
+ tail_start = len(typed_messages)
244
+ base = dict(body)
245
+ for index in range(len(typed_messages) - 1, -1, -1):
246
+ candidate_body = dict(base)
247
+ candidate_body["messages"] = typed_messages[index:]
248
+ if runtime.estimate_tokens(candidate_body) <= tail_budget:
249
+ tail_start = index
250
+ continue
251
+ break
252
+ while tail_start < len(typed_messages) and not anthropic_safe_tail_start(typed_messages[tail_start]):
253
+ tail_start += 1
254
+ if tail_start >= len(typed_messages):
255
+ tail_start = max(0, len(typed_messages) - 1)
256
+ latest = typed_messages[tail_start]
257
+ if anthropic_message_has_tool_result(latest):
258
+ safe_text = text.content_to_text(latest.get("content"))
259
+ tail = [{"role": "user", "content": text.compact_text(safe_text)}]
260
+ else:
261
+ tail = [latest]
262
+ else:
263
+ tail = typed_messages[tail_start:]
264
+ omitted = typed_messages[:tail_start]
265
+ summary_text = text.build_summary(omitted, budget_tokens)
266
+ out = dict(body)
267
+ out["messages"] = tail
268
+ out["system"] = text.append_system_texts(body.get("system"), [summary_text])
269
+ while runtime.estimate_tokens(out) > budget_tokens and len(tail) > 1:
270
+ tail = tail[1:]
271
+ while tail and not anthropic_safe_tail_start(tail[0]):
272
+ tail = tail[1:]
273
+ if not tail:
274
+ tail = [typed_messages[-1]]
275
+ break
276
+ out["messages"] = tail
277
+ omitted = typed_messages[: len(typed_messages) - len(tail)]
278
+ out["system"] = text.append_system_texts(
279
+ body.get("system"),
280
+ [text.build_summary(omitted, budget_tokens)],
281
+ )
282
+ final_tokens = runtime.estimate_tokens(out)
283
+ if final_tokens > budget_tokens:
284
+ compact_summary = text.build_summary(omitted, max(8192, budget_tokens // 2))
285
+ out["system"] = text.append_system_texts(
286
+ body.get("system"),
287
+ [text.truncate(compact_summary, max(4096, budget_tokens * 2))],
288
+ )
289
+ final_tokens = runtime.estimate_tokens(out)
290
+ if final_tokens > budget_tokens and tail:
291
+ base_without_messages = dict(out)
292
+ base_without_messages["messages"] = []
293
+ remaining_chars = max(1024, (budget_tokens - runtime.estimate_tokens(base_without_messages)) * 4 - 1024)
294
+ latest = tail[-1]
295
+ latest_role = str(latest.get("role") or "unknown")
296
+ latest_text = text.content_to_text(latest.get("content"))
297
+ out["messages"] = [{
298
+ "role": "user",
299
+ "content": text.truncate(
300
+ f"[Latest retained message compacted from role={latest_role} because it alone exceeded the provider context budget.]\n{latest_text}",
301
+ remaining_chars,
302
+ ),
303
+ }]
304
+ final_tokens = runtime.estimate_tokens(out)
305
+ runtime.log(
306
+ "WARN",
307
+ f"compacted anthropic payload messages {len(typed_messages)}->{len(out.get('messages') or [])} tokens {initial_tokens}->{final_tokens} budget={budget_tokens}",
308
+ )
309
+ chunk_count = text.chunk_count(omitted, budget_tokens)
310
+ if chunk_count and (provider or model):
311
+ runtime.write_activity(
312
+ provider or "provider",
313
+ model or str(body.get("model") or ""),
314
+ chunks=chunk_count,
315
+ parallel_sessions=1,
316
+ tokens=initial_tokens,
317
+ final_tokens=final_tokens,
318
+ budget=budget_tokens,
319
+ omitted_messages=len(omitted),
320
+ retained_messages=len(out.get("messages") or []),
321
+ )
322
+ return out
@@ -0,0 +1,176 @@
1
+ """Protocol-aware system prompt transformation policies.
2
+
3
+ The router calls one application service while wire-format strategies own the
4
+ placement details. This keeps cross-cutting prompt features independent from
5
+ providers: a provider only declares its protocol and does not need custom
6
+ prompt mutation code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable, Mapping, Protocol
13
+
14
+ from ciel_runtime_support.architecture import MessageProtocol
15
+
16
+
17
+ JsonObject = dict[str, Any]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SystemPromptInjection:
22
+ """A provider-neutral request to add trusted system context."""
23
+
24
+ texts: tuple[str, ...]
25
+
26
+ @classmethod
27
+ def from_texts(cls, texts: list[str] | tuple[str, ...] | None) -> SystemPromptInjection:
28
+ return cls(tuple(clean for item in (texts or ()) if (clean := str(item or "").strip())))
29
+
30
+ @property
31
+ def empty(self) -> bool:
32
+ return not self.texts
33
+
34
+
35
+ class PromptInjectionStrategy(Protocol):
36
+ """Wire-format strategy used by :class:`PromptInjector`."""
37
+
38
+ def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject: ...
39
+
40
+
41
+ def append_anthropic_system_texts(system: Any, texts: list[str] | tuple[str, ...] | None) -> Any:
42
+ """Append Anthropic text blocks without mutating or reordering existing blocks."""
43
+
44
+ injection = SystemPromptInjection.from_texts(texts)
45
+ if injection.empty:
46
+ return system
47
+ blocks: list[Any]
48
+ if isinstance(system, list):
49
+ blocks = [dict(block) if isinstance(block, dict) else block for block in system]
50
+ elif isinstance(system, str):
51
+ blocks = [{"type": "text", "text": system.strip()}] if system.strip() else []
52
+ elif system:
53
+ blocks = [{"type": "text", "text": str(system).strip()}]
54
+ else:
55
+ blocks = []
56
+ blocks.extend({"type": "text", "text": text} for text in injection.texts)
57
+ return blocks
58
+
59
+
60
+ class AnthropicPromptInjectionStrategy:
61
+ def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
62
+ out = dict(body)
63
+ out["system"] = append_anthropic_system_texts(body.get("system"), injection.texts)
64
+ return out
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class ChatPromptInjectionStrategy:
69
+ """Insert context after existing system/developer messages."""
70
+
71
+ role: str = "system"
72
+
73
+ def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
74
+ messages = body.get("messages")
75
+ copied = [dict(item) if isinstance(item, dict) else item for item in messages] if isinstance(messages, list) else []
76
+ insertion = 0
77
+ while insertion < len(copied):
78
+ item = copied[insertion]
79
+ if not isinstance(item, dict) or str(item.get("role") or "") not in {"system", "developer"}:
80
+ break
81
+ insertion += 1
82
+ copied[insertion:insertion] = ({"role": self.role, "content": text} for text in injection.texts)
83
+ out = dict(body)
84
+ out["messages"] = copied
85
+ return out
86
+
87
+
88
+ class OpenAIResponsesPromptInjectionStrategy:
89
+ def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
90
+ out = dict(body)
91
+ parts = [str(body.get("instructions") or "").strip(), *injection.texts]
92
+ out["instructions"] = "\n\n".join(part for part in parts if part)
93
+ return out
94
+
95
+
96
+ class GooglePromptInjectionStrategy:
97
+ """Append Gemini ``systemInstruction.parts`` while preserving key casing."""
98
+
99
+ def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
100
+ out = dict(body)
101
+ key = "system_instruction" if "system_instruction" in body else "systemInstruction"
102
+ current = body.get(key)
103
+ if isinstance(current, dict):
104
+ system_instruction = dict(current)
105
+ raw_parts = current.get("parts")
106
+ parts = [dict(item) if isinstance(item, dict) else item for item in raw_parts] if isinstance(raw_parts, list) else []
107
+ elif isinstance(current, str) and current.strip():
108
+ system_instruction = {}
109
+ parts = [{"text": current.strip()}]
110
+ else:
111
+ system_instruction = {}
112
+ parts = []
113
+ parts.extend({"text": text} for text in injection.texts)
114
+ system_instruction["parts"] = parts
115
+ out[key] = system_instruction
116
+ return out
117
+
118
+
119
+ class PromptInjector:
120
+ """Application service dispatching to protocol transformation strategies."""
121
+
122
+ def __init__(self, strategies: Mapping[MessageProtocol, PromptInjectionStrategy] | None = None) -> None:
123
+ self._strategies: dict[MessageProtocol, PromptInjectionStrategy] = dict(strategies or default_strategies())
124
+
125
+ def inject(
126
+ self,
127
+ body: Mapping[str, Any],
128
+ protocol: MessageProtocol,
129
+ texts: list[str] | tuple[str, ...] | None,
130
+ ) -> JsonObject:
131
+ injection = SystemPromptInjection.from_texts(texts)
132
+ if injection.empty:
133
+ return dict(body)
134
+ try:
135
+ strategy = self._strategies[protocol]
136
+ except KeyError as exc: # pragma: no cover - MessageProtocol keeps this defensive
137
+ raise ValueError(f"unsupported prompt injection protocol: {protocol}") from exc
138
+ return strategy.inject(body, injection)
139
+
140
+
141
+ def default_strategies() -> Mapping[MessageProtocol, PromptInjectionStrategy]:
142
+ chat = ChatPromptInjectionStrategy()
143
+ return {
144
+ "anthropic_messages": AnthropicPromptInjectionStrategy(),
145
+ "openai_chat": chat,
146
+ "openai_responses": OpenAIResponsesPromptInjectionStrategy(),
147
+ "ollama_chat": chat,
148
+ "google_generative": GooglePromptInjectionStrategy(),
149
+ }
150
+
151
+
152
+ def normalize_anthropic_system_role_messages(
153
+ body: Mapping[str, Any],
154
+ content_to_text: Callable[[Any], str],
155
+ ) -> JsonObject:
156
+ """Move non-standard Anthropic system-role messages to top-level blocks."""
157
+
158
+ messages = body.get("messages")
159
+ if not isinstance(messages, list):
160
+ return dict(body)
161
+ next_messages: list[Any] = []
162
+ system_texts: list[str] = []
163
+ changed = False
164
+ for message in messages:
165
+ if not isinstance(message, dict) or str(message.get("role") or "").strip() != "system":
166
+ next_messages.append(message)
167
+ continue
168
+ changed = True
169
+ if text := content_to_text(message.get("content")).strip():
170
+ system_texts.append(text)
171
+ if not changed:
172
+ return dict(body)
173
+ out = dict(body)
174
+ out["messages"] = next_messages
175
+ out["system"] = append_anthropic_system_texts(body.get("system"), system_texts)
176
+ return out
@@ -0,0 +1,24 @@
1
+ """Wire-protocol conversion helpers used by the runtime routers."""
2
+
3
+ from .openai_responses import (
4
+ OpenAIResponsesProtocolAdapter,
5
+ anthropic_message_to_openai_response,
6
+ openai_responses_to_anthropic_messages,
7
+ )
8
+ from ..registry import AdapterRegistry
9
+
10
+
11
+ PROTOCOL_ADAPTERS: AdapterRegistry[OpenAIResponsesProtocolAdapter] = AdapterRegistry()
12
+ PROTOCOL_ADAPTERS.register(
13
+ "openai_responses",
14
+ lambda **kwargs: OpenAIResponsesProtocolAdapter(**kwargs),
15
+ aliases=("openai-responses",),
16
+ )
17
+
18
+
19
+ __all__ = [
20
+ "PROTOCOL_ADAPTERS",
21
+ "OpenAIResponsesProtocolAdapter",
22
+ "anthropic_message_to_openai_response",
23
+ "openai_responses_to_anthropic_messages",
24
+ ]
@@ -0,0 +1,29 @@
1
+ """Pure Anthropic content-block text projection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def content_to_text(content: Any) -> str:
9
+ if isinstance(content, str):
10
+ return content
11
+ if not isinstance(content, list):
12
+ return str(content) if content is not None else ""
13
+ parts: list[str] = []
14
+ for block in content:
15
+ if isinstance(block, str):
16
+ parts.append(block)
17
+ continue
18
+ if not isinstance(block, dict):
19
+ continue
20
+ block_type = block.get("type")
21
+ if block_type == "text":
22
+ parts.append(str(block.get("text", "")))
23
+ elif block_type == "tool_result":
24
+ tool_text = content_to_text(block.get("content", ""))
25
+ parts.append(
26
+ "Tool result for %s:\n%s"
27
+ % (block.get("tool_use_id", "tool"), tool_text)
28
+ )
29
+ return "\n".join(part for part in parts if part)