@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,103 @@
1
+ """Router HTTP server startup and shutdown application service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class RouterServerConfig:
12
+ config_dir: Path
13
+ pid_path: Path
14
+ port: int
15
+ client_base: str
16
+ log_level_path: Path
17
+ log_level_names: dict[Any, Any]
18
+ handler: Any
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class RouterServerStatePorts:
23
+ load_config: Callable[[], dict[str, Any]]
24
+ reset_api_key_cooldowns: Callable[[], None]
25
+ bind_host: Callable[[dict[str, Any]], str]
26
+ current_log_level: Callable[[], Any]
27
+ current_pid: Callable[[], int]
28
+ env_value: Callable[[str], str | None]
29
+ external_access_enabled: Callable[[dict[str, Any]], bool]
30
+ ensure_external_token: Callable[[], str]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class RouterServerEffects:
35
+ chmod: Callable[[Path, int], None]
36
+ stderr: Any
37
+ server_factory: Callable[..., Any]
38
+ start_watchdog: Callable[[Any], None]
39
+ start_channels: Callable[[dict[str, Any]], Any]
40
+ stop_channels: Callable[[None], Any]
41
+ thread_factory: Callable[..., Any]
42
+ configure_web_endpoints: Callable[[str], list[str]]
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class RouterServerRuntime:
47
+ config: RouterServerConfig
48
+ state: RouterServerStatePorts
49
+ effects: RouterServerEffects
50
+
51
+ def run(self) -> None:
52
+ self.config.config_dir.mkdir(parents=True, exist_ok=True)
53
+ runtime_config = self.state.load_config()
54
+ self.state.reset_api_key_cooldowns()
55
+ bind_host = self.state.bind_host(runtime_config)
56
+ if self.state.external_access_enabled(runtime_config):
57
+ self.state.ensure_external_token()
58
+ self.config.pid_path.write_text(str(self.state.current_pid()))
59
+ self.effects.chmod(self.config.pid_path, 0o600)
60
+ level = self.state.current_log_level()
61
+ source = self._log_level_source()
62
+ self.effects.stderr.write(
63
+ f"ciel-runtime router starting on {bind_host}:{self.config.port} "
64
+ f"(client base {self.config.client_base}, log level "
65
+ f"{self.config.log_level_names.get(level, level)}, source={source})\n"
66
+ )
67
+ self.effects.stderr.flush()
68
+ server = self.effects.server_factory(
69
+ (bind_host, self.config.port), self.config.handler
70
+ )
71
+ for line in self.effects.configure_web_endpoints(bind_host):
72
+ self.effects.stderr.write(f"{line}\n")
73
+ self.effects.stderr.flush()
74
+ self.effects.start_watchdog(server)
75
+ channel_thread = self.effects.thread_factory(
76
+ target=lambda: self.effects.start_channels(runtime_config),
77
+ daemon=True,
78
+ name="ca-router-channel-sse-start",
79
+ )
80
+ channel_thread.start()
81
+ try:
82
+ server.serve_forever()
83
+ finally:
84
+ self.effects.stop_channels(None)
85
+ try:
86
+ self.config.pid_path.unlink()
87
+ except FileNotFoundError:
88
+ pass
89
+
90
+ def _log_level_source(self) -> str:
91
+ if self.config.log_level_path.exists():
92
+ return "file"
93
+ if self.state.env_value("CIEL_RUNTIME_LOG_LEVEL"):
94
+ return "env"
95
+ return "default"
96
+
97
+
98
+ __all__ = [
99
+ "RouterServerConfig",
100
+ "RouterServerEffects",
101
+ "RouterServerRuntime",
102
+ "RouterServerStatePorts",
103
+ ]
@@ -0,0 +1,201 @@
1
+ """Local router slash-command short-circuit controllers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Protocol
8
+
9
+
10
+ class ShortcutHandler(Protocol):
11
+ pass
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ShortcutResponsePorts:
16
+ load_config: Callable[[], dict[str, Any]]
17
+ current_alias: Callable[[dict[str, Any]], str]
18
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
19
+ write_anthropic: Callable[..., Any]
20
+ publish_event: Callable[..., Any]
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class ShortcutPredicates:
25
+ router_debug: Callable[[dict[str, Any]], bool]
26
+ version: Callable[[dict[str, Any]], bool]
27
+ channel_clear: Callable[[dict[str, Any]], bool]
28
+ live_llm_options: Callable[[dict[str, Any]], bool]
29
+ live_api_keys: Callable[[dict[str, Any]], bool]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class RouterDebugShortcutPorts:
34
+ value: Callable[[dict[str, Any]], str]
35
+ external_enabled: Callable[[dict[str, Any]], bool]
36
+ bind_host: Callable[[dict[str, Any]], str]
37
+ set_external: Callable[[Any], list[str]]
38
+ schedule_restart: Callable[[], Any]
39
+ version: str
40
+ source_fingerprint: str
41
+ config_dir: Path
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class ChannelShortcutPorts:
46
+ value: Callable[[dict[str, Any]], str]
47
+ clear: Callable[[], dict[str, Any]]
48
+ status: Callable[[], dict[str, Any]]
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class LiveConfigShortcutPorts:
53
+ llm_value: Callable[[dict[str, Any]], str]
54
+ handle_llm: Callable[[str], tuple[list[str], bool]]
55
+ api_key_value: Callable[[dict[str, Any]], str]
56
+ handle_api_keys: Callable[[str], tuple[list[str], bool]]
57
+ api_key_count: Callable[[str, dict[str, Any]], int]
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class RouterShortcutController:
62
+ response: ShortcutResponsePorts
63
+ predicates: ShortcutPredicates
64
+ debug: RouterDebugShortcutPorts
65
+ channel: ChannelShortcutPorts
66
+ live: LiveConfigShortcutPorts
67
+
68
+ def _model_and_stream(self, body: dict[str, Any]) -> tuple[str, bool]:
69
+ config = self.response.load_config()
70
+ return (
71
+ str(body.get("model") or self.response.current_alias(config)),
72
+ bool(body.get("stream", True)),
73
+ )
74
+
75
+ def handle_router_debug(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
76
+ if not self.predicates.router_debug(body):
77
+ return False
78
+ config = self.response.load_config()
79
+ current = self.debug.external_enabled(config)
80
+ value = self.debug.value(body).strip().lower()
81
+ should_restart = False
82
+ if value in {"", "status", "state", "show", "?"}:
83
+ lines = [
84
+ f"Router debug external access: {'on' if current else 'off'}.",
85
+ f"Current router bind host: {self.debug.bind_host(config)}.",
86
+ ]
87
+ elif value in {"toggle", "tog", "switch"}:
88
+ lines = self.debug.set_external(not current)
89
+ should_restart = True
90
+ elif value in {"on", "true", "1", "enable", "enabled"}:
91
+ lines = self.debug.set_external(True)
92
+ should_restart = True
93
+ elif value in {"off", "false", "0", "disable", "disabled"}:
94
+ lines = self.debug.set_external(False)
95
+ should_restart = True
96
+ else:
97
+ lines = [
98
+ "Usage: `/router-debug`, `/router-debug on`, `/router-debug off`, or `/router-debug status`."
99
+ ]
100
+ if should_restart:
101
+ lines.append("Router restart scheduled so the bind address changes immediately.")
102
+ model, stream = self._model_and_stream(body)
103
+ self.response.write_anthropic(handler, model, "\n".join(lines), stream)
104
+ if should_restart:
105
+ self.debug.schedule_restart()
106
+ return True
107
+
108
+ def handle_version(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
109
+ if not self.predicates.version(body):
110
+ return False
111
+ lines = [
112
+ f"ciel-runtime {self.debug.version}",
113
+ f"source: {self.debug.source_fingerprint[:12]}",
114
+ f"config dir: {self.debug.config_dir}",
115
+ ]
116
+ model, stream = self._model_and_stream(body)
117
+ self.response.write_anthropic(handler, model, "\n".join(lines), stream)
118
+ return True
119
+
120
+ @staticmethod
121
+ def channel_status_lines(stats: dict[str, Any], cleared: bool) -> list[str]:
122
+ if cleared:
123
+ return [
124
+ "Ciel Runtime channel backlog discarded.",
125
+ f"- chat tail: {stats.get('chat_tail')}",
126
+ f"- LLM cursor advanced by: {stats.get('discarded_llm')}",
127
+ f"- MCP cursor advanced by: {stats.get('discarded_mcp')}",
128
+ f"- active MCP channel sessions updated: {stats.get('mcp_sessions_updated')}",
129
+ "New channel events arriving after this point will still be delivered.",
130
+ ]
131
+ return [
132
+ "Ciel Runtime channel backlog status.",
133
+ f"- chat tail: {stats.get('chat_tail')}",
134
+ f"- pending LLM items by id range: {stats.get('pending_llm')}",
135
+ f"- pending MCP items by id range: {stats.get('pending_mcp')}",
136
+ f"- active MCP channel sessions: {stats.get('mcp_sessions')}",
137
+ ]
138
+
139
+ def handle_channel_clear(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
140
+ if not self.predicates.channel_clear(body):
141
+ return False
142
+ value = self.channel.value(body).strip().lower()
143
+ if value in {"", "all", "clear", "discard", "drop", "purge", "reset", "now"}:
144
+ lines = self.channel_status_lines(self.channel.clear(), cleared=True)
145
+ elif value in {"status", "state", "show", "?", "dry-run", "dryrun"}:
146
+ lines = self.channel_status_lines(self.channel.status(), cleared=False)
147
+ else:
148
+ lines = ["Usage: `/channel-clear`, `/channel-clear all`, or `/channel-clear status`."]
149
+ model, stream = self._model_and_stream(body)
150
+ self.response.write_anthropic(handler, model, "\n".join(lines), stream)
151
+ return True
152
+
153
+ def handle_live_llm_options(
154
+ self, handler: ShortcutHandler, body: dict[str, Any]
155
+ ) -> bool:
156
+ if not self.predicates.live_llm_options(body):
157
+ return False
158
+ value = self.live.llm_value(body)
159
+ lines, changed = self.live.handle_llm(value)
160
+ if changed:
161
+ provider = self.response.current_provider(self.response.load_config())[0]
162
+ self.response.publish_event(
163
+ level="info",
164
+ category="config.llm",
165
+ message="live LLM options updated from slash command",
166
+ provider=provider,
167
+ data={"value": value},
168
+ )
169
+ model, stream = self._model_and_stream(body)
170
+ self.response.write_anthropic(handler, model, "\n".join(lines), stream)
171
+ return True
172
+
173
+ def handle_live_api_keys(
174
+ self, handler: ShortcutHandler, body: dict[str, Any]
175
+ ) -> bool:
176
+ if not self.predicates.live_api_keys(body):
177
+ return False
178
+ value = self.live.api_key_value(body)
179
+ lines, changed = self.live.handle_api_keys(value)
180
+ if changed:
181
+ provider, config = self.response.current_provider(self.response.load_config())
182
+ self.response.publish_event(
183
+ level="info",
184
+ category="config.api_key",
185
+ message="live API key settings updated from slash command",
186
+ provider=provider,
187
+ data={"key_count": self.live.api_key_count(provider, config)},
188
+ )
189
+ model, stream = self._model_and_stream(body)
190
+ self.response.write_anthropic(handler, model, "\n".join(lines), stream)
191
+ return True
192
+
193
+
194
+ __all__ = [
195
+ "ChannelShortcutPorts",
196
+ "LiveConfigShortcutPorts",
197
+ "RouterDebugShortcutPorts",
198
+ "RouterShortcutController",
199
+ "ShortcutPredicates",
200
+ "ShortcutResponsePorts",
201
+ ]
@@ -0,0 +1,73 @@
1
+ """Deterministic provider/model fallback planning policies.
2
+
3
+ The policy decides *what may be tried*; transports remain responsible for
4
+ executing requests. This separation prevents retry mechanics, credentials,
5
+ and provider wire formats from becoming one stateful conditional block.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Iterable, Literal
12
+
13
+
14
+ FallbackReason = Literal["rate_limit", "unavailable", "timeout", "model_error"]
15
+ DEFAULT_FALLBACK_REASONS: frozenset[FallbackReason] = frozenset(
16
+ {"rate_limit", "unavailable", "timeout"}
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class RouteTarget:
22
+ provider: str
23
+ model: str
24
+
25
+ @property
26
+ def identity(self) -> tuple[str, str]:
27
+ return (self.provider.strip().lower().replace("_", "-"), self.model.strip())
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class FallbackRule:
32
+ target: RouteTarget
33
+ reasons: frozenset[FallbackReason] = DEFAULT_FALLBACK_REASONS
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class FallbackPlan:
38
+ primary: RouteTarget
39
+ rules: tuple[FallbackRule, ...] = ()
40
+
41
+ def candidates(self, reason: FallbackReason) -> tuple[RouteTarget, ...]:
42
+ seen: set[tuple[str, str]] = set()
43
+ selected: list[RouteTarget] = []
44
+ for target in (self.primary, *(rule.target for rule in self.rules if reason in rule.reasons)):
45
+ if not target.provider.strip() or not target.model.strip() or target.identity in seen:
46
+ continue
47
+ seen.add(target.identity)
48
+ selected.append(target)
49
+ return tuple(selected)
50
+
51
+
52
+ def build_fallback_plan(
53
+ provider: str,
54
+ model: str,
55
+ configured: Iterable[dict[str, object]] = (),
56
+ ) -> FallbackPlan:
57
+ """Parse a small configuration surface into an immutable routing plan."""
58
+
59
+ rules: list[FallbackRule] = []
60
+ valid_reasons = {"rate_limit", "unavailable", "timeout", "model_error"}
61
+ for item in configured:
62
+ target_provider = str(item.get("provider") or provider).strip()
63
+ target_model = str(item.get("model") or "").strip()
64
+ raw_reasons = item.get("reasons")
65
+ values = raw_reasons if isinstance(raw_reasons, (list, tuple, set, frozenset)) else ()
66
+ reasons = frozenset(str(value) for value in values if str(value) in valid_reasons)
67
+ rules.append(
68
+ FallbackRule(
69
+ RouteTarget(target_provider, target_model),
70
+ reasons=reasons or DEFAULT_FALLBACK_REASONS,
71
+ )
72
+ )
73
+ return FallbackPlan(RouteTarget(provider, model), tuple(rules))
@@ -0,0 +1,143 @@
1
+ """Atomic repository for router and context activity snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class RuntimeActivityPaths:
13
+ router: Path
14
+ context_compact: Path
15
+ context_usage: Path
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class RuntimeActivityClock:
20
+ epoch: Callable[[], float]
21
+ display: Callable[[], str]
22
+ temporary_suffix: Callable[[], str]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class RuntimeActivityEffects:
27
+ publish: Callable[..., Any]
28
+ log: Callable[[str, str], None]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class RuntimeActivityRepository:
33
+ paths: RuntimeActivityPaths
34
+ clock: RuntimeActivityClock
35
+ effects: RuntimeActivityEffects
36
+
37
+ def router_activity(
38
+ self,
39
+ event: str,
40
+ provider: str,
41
+ model: str | None = None,
42
+ **fields: Any,
43
+ ) -> None:
44
+ try:
45
+ level = "error" if event == "error" else ("warn" if event in {"retry", "wait"} else "info")
46
+ self.effects.publish(
47
+ level=level,
48
+ category=f"router.{event}",
49
+ message=f"{event} {provider} {model or ''}".strip(),
50
+ provider=provider,
51
+ model=model,
52
+ data=fields,
53
+ )
54
+ self._write(
55
+ self.paths.router,
56
+ {"event": event, "provider": provider, "model": model or "", **fields},
57
+ )
58
+ except Exception as exc:
59
+ self._report_failure("router", exc)
60
+
61
+ def context_compact(
62
+ self,
63
+ provider: str,
64
+ model: str | None = None,
65
+ **fields: Any,
66
+ ) -> None:
67
+ try:
68
+ self._write(
69
+ self.paths.context_compact,
70
+ {"event": "compact", "provider": provider, "model": model or "", **fields},
71
+ )
72
+ self.effects.publish(
73
+ level="info",
74
+ category="context.compact",
75
+ message=f"compact {provider} {model or ''}".strip(),
76
+ provider=provider,
77
+ model=model,
78
+ data=fields,
79
+ )
80
+ except Exception as exc:
81
+ self._report_failure("context_compact", exc)
82
+
83
+ def context_usage(
84
+ self,
85
+ provider: str,
86
+ provider_config: dict[str, Any],
87
+ body: dict[str, Any],
88
+ source: str,
89
+ *,
90
+ estimate_tokens: Callable[[dict[str, Any]], int],
91
+ context_limit: Callable[[str, dict[str, Any]], int | None],
92
+ ) -> None:
93
+ try:
94
+ tokens = estimate_tokens(body)
95
+ limit = context_limit(provider, provider_config)
96
+ percent = round((tokens / limit) * 100.0, 1) if limit else None
97
+ model = str(body.get("model") or provider_config.get("current_model") or "")
98
+ details = {
99
+ "source": source,
100
+ "tokens": tokens,
101
+ "context_limit": limit,
102
+ "percent": percent,
103
+ "messages": len(body.get("messages") or []),
104
+ "tools": len(body.get("tools") or []),
105
+ }
106
+ self.effects.publish(
107
+ level="debug",
108
+ category="context.usage",
109
+ message=f"context usage {tokens}/{limit or '?'} tokens",
110
+ provider=provider,
111
+ model=model,
112
+ data=details,
113
+ )
114
+ self._write(self.paths.context_usage, {"provider": provider, "model": model, **details})
115
+ except Exception as exc:
116
+ self._report_failure("context_usage", exc)
117
+
118
+ def _write(self, path: Path, data: dict[str, Any]) -> None:
119
+ record = {"updated_at": self.clock.epoch(), "time": self.clock.display(), **data}
120
+ path.parent.mkdir(parents=True, exist_ok=True)
121
+ temporary = path.with_name(f"{path.name}.{self.clock.temporary_suffix()}.tmp")
122
+ temporary.write_text(
123
+ json.dumps(record, ensure_ascii=False, separators=(",", ":")),
124
+ encoding="utf-8",
125
+ )
126
+ temporary.replace(path)
127
+
128
+ def _report_failure(self, operation: str, exc: BaseException) -> None:
129
+ try:
130
+ self.effects.log(
131
+ "WARN",
132
+ f"runtime_activity_{operation}_failed error={type(exc).__name__}: {exc}",
133
+ )
134
+ except Exception:
135
+ return
136
+
137
+
138
+ __all__ = [
139
+ "RuntimeActivityClock",
140
+ "RuntimeActivityEffects",
141
+ "RuntimeActivityPaths",
142
+ "RuntimeActivityRepository",
143
+ ]
@@ -0,0 +1,104 @@
1
+ """Concrete runtime adapter used while legacy launchers are incrementally decomposed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Mapping
8
+
9
+ from .architecture import LaunchSpec, RuntimeAdapter, RuntimeCommand
10
+ from .registry import AdapterRegistry
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CliRuntimeAdapter(RuntimeAdapter):
15
+ """Materialize an already-normalized CLI launch through the runtime contract."""
16
+
17
+ name: str
18
+ executable: str
19
+ environment: Mapping[str, str] = field(default_factory=dict)
20
+ channel_injection: bool = False
21
+
22
+ def find_executable(self) -> Path | None:
23
+ return Path(self.executable) if self.executable else None
24
+
25
+ def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
26
+ executable = spec.runtime.executable or self.executable
27
+ if not executable:
28
+ raise RuntimeError(f"{self.name} runtime executable is unavailable")
29
+ prefix = spec.runtime.options.get("prefix_args", ())
30
+ prefix_args = tuple(str(value) for value in prefix) if isinstance(prefix, (list, tuple)) else ()
31
+ return RuntimeCommand(
32
+ argv=(str(executable), *prefix_args, *spec.passthrough),
33
+ env=dict(self.environment),
34
+ cwd=spec.cwd,
35
+ )
36
+
37
+ def mcp_config_paths(self, spec: LaunchSpec) -> tuple[Path, ...]:
38
+ return spec.runtime.mcp_config_paths
39
+
40
+ def supports_channel_injection(self, spec: LaunchSpec) -> bool:
41
+ return bool(self.channel_injection and spec.runtime.enable_channels)
42
+
43
+
44
+ class ClaudeRuntimeAdapter(CliRuntimeAdapter):
45
+ def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
46
+ options = spec.runtime.options
47
+ argv = [str(spec.runtime.executable or self.executable), "--dangerously-skip-permissions"]
48
+ if options.get("bypass_permission_mode"):
49
+ argv.extend(("--permission-mode", "bypassPermissions"))
50
+ disallowed = str(options.get("disallowed_tools") or "")
51
+ if disallowed:
52
+ argv.extend(("--disallowedTools", disallowed))
53
+ model = str(options.get("model") or "")
54
+ if model:
55
+ argv.extend(("--model", model))
56
+ argv.extend(str(value) for value in options.get("extra_args", ()))
57
+ if options.get("passthrough_boundary"):
58
+ argv.append("--")
59
+ argv.extend(spec.passthrough)
60
+ return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
61
+
62
+
63
+ class CodexRuntimeAdapter(CliRuntimeAdapter):
64
+ def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
65
+ options = spec.runtime.options
66
+ argv = [str(spec.runtime.executable or self.executable)]
67
+ argv.extend(str(value) for value in options.get("yolo_args", ()))
68
+ if spec.mode == "native":
69
+ argv.extend(str(value) for value in options.get("model_args", ()))
70
+ elif spec.mode == "routed":
71
+ argv.extend(str(value) for value in options.get("routed_config_args", ()))
72
+ argv.extend(str(value) for value in options.get("model_args", ()))
73
+ else:
74
+ argv.extend(str(value) for value in options.get("router_config_args", ()))
75
+ if spec.mode != "native":
76
+ argv.extend(str(value) for value in options.get("model_catalog_args", ()))
77
+ for key in ("alternate_screen_args", "mcp_args", "model_alias_args"):
78
+ argv.extend(str(value) for value in options.get(key, ()))
79
+ argv.extend(spec.passthrough)
80
+ return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
81
+
82
+
83
+ class AgyRuntimeAdapter(CliRuntimeAdapter):
84
+ def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
85
+ options = spec.runtime.options
86
+ argv = [str(spec.runtime.executable or self.executable)]
87
+ argv.extend(str(value) for value in options.get("dangerous_args", ()))
88
+ argv.extend(spec.passthrough)
89
+ return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
90
+
91
+
92
+ RUNTIME_ADAPTERS: AdapterRegistry[RuntimeAdapter] = AdapterRegistry()
93
+ RUNTIME_ADAPTERS.register("claude", lambda **kwargs: ClaudeRuntimeAdapter(name="claude", **kwargs))
94
+ RUNTIME_ADAPTERS.register("codex", lambda **kwargs: CodexRuntimeAdapter(name="codex", **kwargs))
95
+ RUNTIME_ADAPTERS.register("agy", lambda **kwargs: AgyRuntimeAdapter(name="agy", **kwargs))
96
+
97
+
98
+ __all__ = [
99
+ "RUNTIME_ADAPTERS",
100
+ "AgyRuntimeAdapter",
101
+ "ClaudeRuntimeAdapter",
102
+ "CliRuntimeAdapter",
103
+ "CodexRuntimeAdapter",
104
+ ]