@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,433 @@
1
+ """Repository and application service for MCP channel capability probes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections.abc import Callable, Iterable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ChannelProbePorts:
15
+ read_servers: Callable[[Path, Path], list[tuple[str, dict[str, Any]]]]
16
+ is_stdio: Callable[[dict[str, Any]], bool]
17
+ probe_stdio: Callable[..., dict[str, Any]]
18
+ probe_sse: Callable[..., dict[str, Any]]
19
+ probe_http: Callable[..., dict[str, Any]]
20
+ log: Callable[[str, str], None]
21
+
22
+
23
+ class ChannelProbeCacheRepository:
24
+ def __init__(
25
+ self,
26
+ path: Path,
27
+ version: int,
28
+ save: Callable[[dict[str, Any], str], None],
29
+ log: Callable[[str, str], None],
30
+ ) -> None:
31
+ self.path = path
32
+ self.version = version
33
+ self.save = save
34
+ self.log = log
35
+
36
+ def empty(self) -> dict[str, Any]:
37
+ return {"version": self.version, "probed_at": 0.0, "servers": []}
38
+
39
+ def read(self) -> dict[str, Any]:
40
+ if not self.path.exists():
41
+ return self.empty()
42
+ try:
43
+ data = json.loads(self.path.read_text(encoding="utf-8"))
44
+ except (OSError, UnicodeError, TypeError, ValueError) as exc:
45
+ self.log(
46
+ "WARN",
47
+ f"channel_probe_cache_read_failed error={type(exc).__name__}: {exc}",
48
+ )
49
+ return self.empty()
50
+ if not isinstance(data, dict):
51
+ return self.empty()
52
+ data.setdefault("version", self.version)
53
+ data.setdefault("probed_at", 0.0)
54
+ servers = data.get("servers")
55
+ data["servers"] = (
56
+ [item for item in servers if isinstance(item, dict)]
57
+ if isinstance(servers, list)
58
+ else []
59
+ )
60
+ return data
61
+
62
+ def write(self, cache: dict[str, Any]) -> None:
63
+ self.save(cache, "channel_probe_cache")
64
+
65
+
66
+ class ChannelProbeService:
67
+ def __init__(
68
+ self,
69
+ router_base: str,
70
+ repository: ChannelProbeCacheRepository,
71
+ ports: ChannelProbePorts,
72
+ config_paths: Callable[..., list[Path]],
73
+ dedupe: Callable[[Iterable[str]], list[str]],
74
+ path_key: Callable[[Path], str],
75
+ native_router_names: frozenset[str],
76
+ ) -> None:
77
+ self.router_base = router_base
78
+ self.repository = repository
79
+ self.ports = ports
80
+ self.config_paths = config_paths
81
+ self.dedupe = dedupe
82
+ self.path_key = path_key
83
+ self.native_router_names = native_router_names
84
+
85
+ def builtin_record(self) -> dict[str, Any]:
86
+ return {
87
+ "name": "ciel-runtime-router",
88
+ "capable": True,
89
+ "transport": "sse",
90
+ "source_path": "<built-in>",
91
+ "url": f"{self.router_base}/ca/mcp/sse",
92
+ "response_bytes": 0,
93
+ "reason": "built-in",
94
+ }
95
+
96
+ @staticmethod
97
+ def transport_label(server: dict[str, Any]) -> str:
98
+ if not isinstance(server, dict):
99
+ return "unknown"
100
+ declared = str(server.get("type") or "").strip().lower()
101
+ if declared in {"http", "streamable-http"}:
102
+ return "streamable-http"
103
+ if declared:
104
+ return declared
105
+ if server.get("url"):
106
+ return "sse"
107
+ if server.get("command"):
108
+ return "stdio"
109
+ return "unknown"
110
+
111
+ def probe(
112
+ self,
113
+ paths: Iterable[str],
114
+ cwd: Path,
115
+ *,
116
+ include_router_self: bool = True,
117
+ timeout_per_server: float | None = None,
118
+ ) -> list[dict[str, Any]]:
119
+ records: list[dict[str, Any]] = []
120
+ seen: set[str] = set()
121
+ if include_router_self:
122
+ records.append(self.builtin_record())
123
+ seen.add("ciel-runtime-router")
124
+ for path_text in paths:
125
+ path = Path(path_text) if path_text else Path()
126
+ if not path_text or not path.is_file():
127
+ continue
128
+ for name, server in self.ports.read_servers(path, cwd):
129
+ if name in seen:
130
+ continue
131
+ seen.add(name)
132
+ if name == "ciel-runtime-router":
133
+ continue
134
+ record = self._probe_record(
135
+ name, server, path, timeout_per_server
136
+ )
137
+ records.append(record)
138
+ return records
139
+
140
+ def _probe_record(
141
+ self,
142
+ name: str,
143
+ server: dict[str, Any],
144
+ path: Path,
145
+ timeout: float | None,
146
+ ) -> dict[str, Any]:
147
+ transport = self.transport_label(server)
148
+ record: dict[str, Any] = {
149
+ "name": name,
150
+ "capable": False,
151
+ "transport": transport,
152
+ "source_path": str(path),
153
+ "response_bytes": 0,
154
+ "response_received": False,
155
+ "elapsed_ms": 0,
156
+ "exit_code": None,
157
+ "stderr_bytes": 0,
158
+ "stderr_preview": "",
159
+ "stdout_preview": "",
160
+ "reason": "",
161
+ }
162
+ if isinstance(server.get("url"), str):
163
+ record["url"] = str(server["url"])
164
+ probe: Callable[..., dict[str, Any]] | None = None
165
+ if self.ports.is_stdio(server):
166
+ probe = self.ports.probe_stdio
167
+ elif transport == "sse" and isinstance(server.get("url"), str):
168
+ probe = self.ports.probe_sse
169
+ elif transport == "streamable-http" and isinstance(server.get("url"), str):
170
+ probe = self.ports.probe_http
171
+ if probe is None:
172
+ record["reason"] = "transport_not_probed"
173
+ return record
174
+ try:
175
+ detail = probe(name, server, timeout=timeout)
176
+ record["exit_code"] = detail.get("exit_code")
177
+ record["stderr_preview"] = str(detail.get("stderr_preview") or "")
178
+ record["stdout_preview"] = str(detail.get("stdout_preview") or "")
179
+ record.update(
180
+ capable=bool(detail.get("capable")),
181
+ reason=str(detail.get("reason") or ""),
182
+ response_bytes=int(detail.get("response_bytes") or 0),
183
+ response_received=bool(detail.get("response_received")),
184
+ elapsed_ms=int(detail.get("elapsed_ms") or 0),
185
+ stderr_bytes=int(detail.get("stderr_bytes") or 0),
186
+ )
187
+ except Exception as exc:
188
+ record["reason"] = f"probe_exception:{type(exc).__name__}"
189
+ self.ports.log(
190
+ "WARN",
191
+ f"channel_probe_exception server={name} "
192
+ f"error={type(exc).__name__}: {exc}",
193
+ )
194
+ return record
195
+
196
+ def refresh(
197
+ self,
198
+ passthrough: list[str] | None = None,
199
+ cwd: Path | None = None,
200
+ home: Path | None = None,
201
+ timeout_per_server: float | None = None,
202
+ extra_config_paths: list[Path | str] | None = None,
203
+ ) -> dict[str, Any]:
204
+ cwd = cwd or Path.cwd()
205
+ paths = [str(Path(path).expanduser()) for path in extra_config_paths or []]
206
+ paths.extend(
207
+ str(path) for path in self.config_paths(passthrough or [], cwd, home)
208
+ )
209
+ records = self.probe(paths, cwd, timeout_per_server=timeout_per_server)
210
+ cache = {
211
+ "version": self.repository.version,
212
+ "probed_at": time.time(),
213
+ "servers": records,
214
+ }
215
+ self.repository.write(cache)
216
+ capable = [str(record["name"]) for record in records if record.get("capable")]
217
+ self.ports.log(
218
+ "INFO",
219
+ f"channel_probe_cache_refreshed total={len(records)} "
220
+ f"capable={len(capable)} servers={','.join(capable) or '-'}",
221
+ )
222
+ return cache
223
+
224
+ def servers(self) -> list[dict[str, Any]]:
225
+ servers = self.repository.read().get("servers")
226
+ return (
227
+ [item for item in servers if isinstance(item, dict)]
228
+ if isinstance(servers, list)
229
+ else []
230
+ )
231
+
232
+ @staticmethod
233
+ def bucket(record: dict[str, Any]) -> str:
234
+ if record.get("capable"):
235
+ return "capable"
236
+ reason = str(record.get("reason") or "").strip()
237
+ if reason == "no_experimental_claude_channel" and record.get(
238
+ "response_received"
239
+ ):
240
+ return "non_capable"
241
+ if reason in {"transport_not_probed", "no_url"}:
242
+ return "skipped"
243
+ return "inconclusive"
244
+
245
+ def capable_names(self) -> list[str]:
246
+ names = [
247
+ str(record.get("name"))
248
+ for record in self.servers()
249
+ if record.get("capable") and record.get("name")
250
+ ]
251
+ if "ciel-runtime-router" not in names:
252
+ names.insert(0, "ciel-runtime-router")
253
+ return self.dedupe(names)
254
+
255
+ def external_capable_names(self) -> list[str]:
256
+ names = []
257
+ for record in self.servers():
258
+ name = str(record.get("name") or "").strip()
259
+ if (
260
+ record.get("capable")
261
+ and name
262
+ and name.lower() not in self.native_router_names
263
+ ):
264
+ names.append(name)
265
+ return self.dedupe(names)
266
+
267
+ def source_paths(self, specs: Iterable[str]) -> list[Path]:
268
+ wanted = {
269
+ str(spec).split(":", 1)[1]
270
+ for spec in specs
271
+ if str(spec).startswith("server:") and str(spec).split(":", 1)[1]
272
+ }
273
+ if not wanted:
274
+ return []
275
+ paths: list[Path] = []
276
+ seen: set[str] = set()
277
+ for record in self.servers():
278
+ if str(record.get("name") or "") not in wanted:
279
+ continue
280
+ source = str(record.get("source_path") or "")
281
+ if not source or source == "<built-in>":
282
+ continue
283
+ path = Path(source).expanduser()
284
+ key = self.path_key(path)
285
+ if key not in seen:
286
+ seen.add(key)
287
+ paths.append(path)
288
+ return paths
289
+
290
+ def server_names_from_specs(self, specs: Iterable[str]) -> list[str]:
291
+ names = []
292
+ for spec in specs:
293
+ text = str(spec or "").strip()
294
+ if text.startswith("server:"):
295
+ name = text.split(":", 1)[1].strip()
296
+ if name:
297
+ names.append(name)
298
+ return self.dedupe(names)
299
+
300
+ def candidate_names(
301
+ self,
302
+ specs: Iterable[str],
303
+ discover: Callable[[], list[str]],
304
+ ) -> list[str]:
305
+ explicit = [
306
+ name
307
+ for name in self.server_names_from_specs(specs)
308
+ if name.lower() not in self.native_router_names
309
+ ]
310
+ try:
311
+ discovered = discover()
312
+ except Exception as exc:
313
+ self.ports.log(
314
+ "WARN",
315
+ f"channel_auto_discovery_failed error={type(exc).__name__}: {exc}",
316
+ )
317
+ discovered = []
318
+ return self.dedupe([*explicit, *discovered])
319
+
320
+ @staticmethod
321
+ def needs_refresh(
322
+ cache: dict[str, Any],
323
+ records: list[dict[str, Any]],
324
+ candidate_names: list[str],
325
+ ) -> bool:
326
+ if not cache.get("probed_at") or not records:
327
+ return True
328
+ if not candidate_names:
329
+ return False
330
+ by_name = {
331
+ str(record.get("name") or ""): record
332
+ for record in records
333
+ if record.get("name")
334
+ }
335
+ for name in candidate_names:
336
+ record = by_name.get(name)
337
+ if not record or not record.get("capable"):
338
+ return True
339
+ source = str(record.get("source_path") or "")
340
+ if not source or source == "<built-in>":
341
+ return True
342
+ return False
343
+
344
+ def ensure_refresh(
345
+ self,
346
+ refresh_needed: bool,
347
+ refresh: Callable[[], Any],
348
+ ) -> bool:
349
+ if not refresh_needed:
350
+ return False
351
+ try:
352
+ self.ports.log(
353
+ "INFO",
354
+ "channel_probe_launch_refresh "
355
+ "reason=missing_cache_or_selected_server",
356
+ )
357
+ refresh()
358
+ return True
359
+ except Exception as exc:
360
+ self.ports.log(
361
+ "WARN",
362
+ f"channel_probe_launch_refresh_failed "
363
+ f"error={type(exc).__name__}: {exc}",
364
+ )
365
+ return False
366
+
367
+
368
+ @dataclass(frozen=True, slots=True)
369
+ class ChannelProbeCompatibilityApi:
370
+ """Typed compatibility and launch-orchestration adapter for channel probes."""
371
+
372
+ service_factory: Callable[[], ChannelProbeService]
373
+
374
+ def builtin_record(self) -> dict[str, Any]:
375
+ return self.service_factory().builtin_record()
376
+
377
+ def transport_label(self, server: dict[str, Any]) -> str:
378
+ return self.service_factory().transport_label(server)
379
+
380
+ def probe(
381
+ self,
382
+ paths: Iterable[str],
383
+ cwd: Path,
384
+ *,
385
+ include_router_self: bool = True,
386
+ timeout_per_server: float | None = None,
387
+ ) -> list[dict[str, Any]]:
388
+ return self.service_factory().probe(
389
+ paths,
390
+ cwd,
391
+ include_router_self=include_router_self,
392
+ timeout_per_server=timeout_per_server,
393
+ )
394
+
395
+ def read_cache(self) -> dict[str, Any]:
396
+ return self.service_factory().repository.read()
397
+
398
+ def write_cache(self, cache: dict[str, Any]) -> None:
399
+ self.service_factory().repository.write(cache)
400
+
401
+ def refresh(
402
+ self,
403
+ passthrough: list[str] | None = None,
404
+ cwd: Path | None = None,
405
+ home: Path | None = None,
406
+ timeout_per_server: float | None = None,
407
+ extra_config_paths: list[Path | str] | None = None,
408
+ ) -> dict[str, Any]:
409
+ return self.service_factory().refresh(
410
+ passthrough,
411
+ cwd,
412
+ home,
413
+ timeout_per_server,
414
+ extra_config_paths,
415
+ )
416
+
417
+ def servers(self) -> list[dict[str, Any]]:
418
+ return self.service_factory().servers()
419
+
420
+ def bucket(self, record: dict[str, Any]) -> str:
421
+ return self.service_factory().bucket(record)
422
+
423
+ def capable_names(self) -> list[str]:
424
+ return self.service_factory().capable_names()
425
+
426
+ def external_capable_names(self) -> list[str]:
427
+ return self.service_factory().external_capable_names()
428
+
429
+ def source_paths(self, specs: Iterable[str]) -> list[Path]:
430
+ return self.service_factory().source_paths(specs)
431
+
432
+ def server_names_from_specs(self, specs: Iterable[str]) -> list[str]:
433
+ return self.service_factory().server_names_from_specs(specs)
@@ -0,0 +1,101 @@
1
+ """Presentation projection for channel capability probe results."""
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
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class ChannelProbeReportServices:
12
+ bucket: Callable[[dict[str, Any]], str]
13
+ format_timestamp: Callable[[float], str]
14
+
15
+
16
+ def channel_probe_report_lines(
17
+ result: dict[str, Any],
18
+ timeout_seconds: float,
19
+ services: ChannelProbeReportServices,
20
+ ) -> list[str]:
21
+ servers = result.get("servers") or []
22
+ grouped = {
23
+ name: [record for record in servers if services.bucket(record) == name]
24
+ for name in ("capable", "non_capable", "inconclusive", "skipped")
25
+ }
26
+ probed_at = result.get("probed_at") or 0
27
+ timestamp = services.format_timestamp(probed_at) if probed_at else "-"
28
+ lines = [f"channel probe complete (probed at {timestamp}, timeout {timeout_seconds:.1f}s per server)"]
29
+ lines.extend(_capable_lines(grouped["capable"]))
30
+ lines.extend(_non_capable_lines(grouped["non_capable"]))
31
+ inconclusive_lines, timeout_seen, exited_seen = _inconclusive_lines(grouped["inconclusive"])
32
+ lines.extend(inconclusive_lines)
33
+ lines.extend(_skipped_lines(grouped["skipped"]))
34
+ if timeout_seen:
35
+ lines.extend(
36
+ (
37
+ " hint: inconclusive timeout means the probe could not finish; the server may still be capable.",
38
+ " increase CIEL_RUNTIME_CHANNEL_PROBE_TIMEOUT_SECONDS if cold start is the cause,",
39
+ " or re-run with the latest ciel-runtime if this was an SSE endpoint event race.",
40
+ )
41
+ )
42
+ if exited_seen:
43
+ lines.append(" hint: exited_without_response means the child died before responding. Check stderr above.")
44
+ return lines
45
+
46
+
47
+ def _capable_lines(records: list[dict[str, Any]]) -> list[str]:
48
+ lines = [f" capable : {len(records)}"]
49
+ for record in records:
50
+ transport = record.get("transport") or "?"
51
+ source = record.get("source_path") or ""
52
+ suffix = " built-in" if source == "<built-in>" else f" {source}"
53
+ lines.append(f" * {record.get('name')} ({transport}){suffix}")
54
+ return lines
55
+
56
+
57
+ def _non_capable_lines(records: list[dict[str, Any]]) -> list[str]:
58
+ lines = [f" non-capable : {len(records)}"]
59
+ for record in records:
60
+ lines.append(
61
+ f" {record.get('name')} ({record.get('transport') or '?'}) reason={record.get('reason') or '-'}"
62
+ )
63
+ return lines
64
+
65
+
66
+ def _inconclusive_lines(records: list[dict[str, Any]]) -> tuple[list[str], bool, bool]:
67
+ lines = [f" inconclusive: {len(records)}"]
68
+ timeout_seen = False
69
+ exited_seen = False
70
+ for record in records:
71
+ reason = record.get("reason") or "-"
72
+ extras = []
73
+ for key, label in (("elapsed_ms", "elapsed"), ("response_bytes", "stdout"), ("stderr_bytes", "stderr")):
74
+ value = record.get(key)
75
+ if isinstance(value, int) and value:
76
+ suffix = "ms" if key == "elapsed_ms" else "B"
77
+ extras.append(f"{label}={value}{suffix}")
78
+ if record.get("exit_code") is not None:
79
+ extras.append(f"exit={record['exit_code']}")
80
+ extra_text = " " + " ".join(extras) if extras else ""
81
+ lines.append(
82
+ f" {record.get('name')} ({record.get('transport') or '?'}) reason={reason}{extra_text}"
83
+ )
84
+ if record.get("stderr_preview"):
85
+ lines.append(f" stderr: {record['stderr_preview']}")
86
+ if record.get("stdout_preview"):
87
+ lines.append(f" stdout: {record['stdout_preview']}")
88
+ timeout_seen = timeout_seen or str(reason).startswith("timeout")
89
+ exited_seen = exited_seen or reason == "exited_without_response"
90
+ return lines, timeout_seen, exited_seen
91
+
92
+
93
+ def _skipped_lines(records: list[dict[str, Any]]) -> list[str]:
94
+ if not records:
95
+ return []
96
+ lines = [f" skipped : {len(records)}"]
97
+ for record in records:
98
+ lines.append(
99
+ f" {record.get('name')} ({record.get('transport') or '?'}) reason={record.get('reason') or '-'}"
100
+ )
101
+ return lines