@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,105 @@
1
+ """Policies and controller for locally synthesized Claude Code tool uses."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class SyntheticTasklistPorts:
10
+ provider_enabled: Callable[[str], bool]
11
+ content_to_text: Callable[[Any], str]
12
+ should_continue: Callable[..., bool]
13
+ now_ms: Callable[[], int]
14
+ log: Callable[[str, str], None]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class SyntheticTasklistPolicy:
19
+ ports: SyntheticTasklistPorts
20
+
21
+ def append(
22
+ self,
23
+ message: dict[str, Any],
24
+ model: str,
25
+ source_body: dict[str, Any],
26
+ reason: str,
27
+ provider: str = "",
28
+ ) -> dict[str, Any]:
29
+ if not self.ports.provider_enabled(provider):
30
+ return message
31
+ content = message.get("content")
32
+ if not isinstance(content, list):
33
+ content = [{"type": "text", "text": self.ports.content_to_text(content)}] if content else []
34
+ tool_calls = [block for block in content if isinstance(block, dict) and block.get("type") == "tool_use"]
35
+ if not self.ports.should_continue(source_body, self.ports.content_to_text(content), tool_calls):
36
+ return message
37
+ projected = dict(message)
38
+ projected["content"] = [
39
+ *content,
40
+ {
41
+ "type": "tool_use",
42
+ "id": f"toolu_ciel_runtime_TaskList_{reason}_{self.ports.now_ms()}",
43
+ "name": "TaskList",
44
+ "input": {},
45
+ },
46
+ ]
47
+ projected["stop_reason"] = "tool_use"
48
+ projected.setdefault("model", model or message.get("model") or "ciel-runtime-router")
49
+ self.ports.log("WARN", f"auto-synthesized TaskList after clarification question ({reason})")
50
+ return projected
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class ForcedPlanModePorts:
55
+ forced_choice_name: Callable[[dict[str, Any]], str]
56
+ should_defer: Callable[..., bool]
57
+ tool_names: Callable[[dict[str, Any]], set[str]]
58
+ plan_mode_active: Callable[[dict[str, Any]], bool]
59
+ synthetic_response: Callable[..., dict[str, Any]]
60
+ write_json: Callable[..., Any]
61
+ log: Callable[[str, str], None]
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class ForcedPlanModeController:
66
+ ports: ForcedPlanModePorts
67
+
68
+ def handle(
69
+ self,
70
+ handler: Any,
71
+ provider: str,
72
+ provider_config: dict[str, Any],
73
+ body: dict[str, Any],
74
+ ) -> bool:
75
+ if provider == "anthropic":
76
+ return False
77
+ name = self.ports.forced_choice_name(body)
78
+ if name != "EnterPlanMode":
79
+ return False
80
+ if self.ports.should_defer(provider, provider_config, body, name):
81
+ self.ports.log(
82
+ "INFO",
83
+ f"deferred forced {name} tool_choice to native Anthropic-compatible upstream because thinking is enabled",
84
+ )
85
+ return False
86
+ available = self.ports.tool_names(body)
87
+ if available and name not in available:
88
+ return False
89
+ if self.ports.plan_mode_active(body):
90
+ self.ports.log("WARN", f"ignored forced {name} tool_choice because plan mode is already active")
91
+ return False
92
+ self.ports.log("INFO", f"synthesized {name} tool_use for {provider} forced tool_choice")
93
+ self.ports.write_json(
94
+ handler,
95
+ self.ports.synthetic_response(str(body.get("model") or ""), name, {}),
96
+ )
97
+ return True
98
+
99
+
100
+ __all__ = [
101
+ "ForcedPlanModeController",
102
+ "ForcedPlanModePorts",
103
+ "SyntheticTasklistPolicy",
104
+ "SyntheticTasklistPorts",
105
+ ]
@@ -0,0 +1,127 @@
1
+ """Terminal sizing, POSIX PTY sizing, and input-mode reset policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from collections.abc import Callable, Mapping
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+
14
+ TERMINAL_INPUT_MODE_RESET = (
15
+ "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l"
16
+ "\x1b[?1005l\x1b[?1006l\x1b[?1015l"
17
+ )
18
+
19
+
20
+ def read_clipboard_text() -> str:
21
+ if os.name == "nt":
22
+ commands = [["powershell", "-NoProfile", "-Command", "Get-Clipboard -Raw"]]
23
+ elif sys.platform == "darwin":
24
+ commands = [["pbpaste"]]
25
+ else:
26
+ commands = [
27
+ ["xclip", "-selection", "clipboard", "-o"],
28
+ ["xsel", "--clipboard", "--output"],
29
+ ]
30
+ for command in commands:
31
+ try:
32
+ process = subprocess.run(
33
+ command,
34
+ text=True,
35
+ stdout=subprocess.PIPE,
36
+ stderr=subprocess.DEVNULL,
37
+ timeout=5,
38
+ )
39
+ if process.returncode == 0 and process.stdout.strip():
40
+ return process.stdout.strip()
41
+ except Exception:
42
+ continue
43
+ return ""
44
+
45
+
46
+ def terminal_winsize_from_fd(fd: int) -> tuple[int, int]:
47
+ """Return terminal size as (rows, columns), never 0x0."""
48
+ try:
49
+ size = os.get_terminal_size(fd)
50
+ rows = int(size.lines)
51
+ columns = int(size.columns)
52
+ except Exception:
53
+ rows = 0
54
+ columns = 0
55
+ if rows > 0 and columns > 0:
56
+ return rows, columns
57
+ fallback = shutil.get_terminal_size((80, 24))
58
+ rows = int(getattr(fallback, "lines", 0) or 0)
59
+ columns = int(getattr(fallback, "columns", 0) or 0)
60
+ if rows <= 0:
61
+ rows = 24
62
+ if columns <= 0:
63
+ columns = 80
64
+ return rows, columns
65
+
66
+
67
+ def apply_pty_winsize(
68
+ pty_fd: int,
69
+ rows: int,
70
+ columns: int,
71
+ *,
72
+ platform_name: str | None = None,
73
+ ) -> bool:
74
+ if (platform_name or os.name) != "posix" or rows <= 0 or columns <= 0:
75
+ return False
76
+ try:
77
+ import fcntl
78
+ import struct
79
+ import termios
80
+
81
+ fcntl.ioctl(
82
+ pty_fd,
83
+ termios.TIOCSWINSZ,
84
+ struct.pack("HHHH", rows, columns, 0, 0),
85
+ )
86
+ return True
87
+ except Exception:
88
+ return False
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class TerminalInputModeResetPolicy:
93
+ platform_name: str
94
+ environment: Mapping[str, str]
95
+ parse_bool: Callable[..., bool]
96
+ default_stream: Callable[[], Any]
97
+
98
+ def enabled(self) -> bool:
99
+ configured = self.environment.get(
100
+ "CIEL_RUNTIME_TERMINAL_INPUT_MODE_RESET"
101
+ )
102
+ if self.platform_name == "nt" and configured is None:
103
+ return False
104
+ return self.parse_bool(configured, True)
105
+
106
+ def interval_seconds(self, default: float = 2.0) -> float:
107
+ raw = self.environment.get(
108
+ "CIEL_RUNTIME_TERMINAL_INPUT_MODE_RESET_INTERVAL_SECONDS"
109
+ )
110
+ if raw is None:
111
+ return default
112
+ try:
113
+ return max(0.25, min(60.0, float(raw)))
114
+ except Exception:
115
+ return default
116
+
117
+ def write(self, stream: Any | None = None) -> None:
118
+ if not self.enabled():
119
+ return
120
+ target = stream if stream is not None else self.default_stream()
121
+ try:
122
+ if hasattr(target, "isatty") and not target.isatty():
123
+ return
124
+ target.write(TERMINAL_INPUT_MODE_RESET)
125
+ target.flush()
126
+ except Exception:
127
+ return
@@ -0,0 +1,196 @@
1
+ """User-selectable timeout profile projection and mutation service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ TimeoutPreset = tuple[int, str, str]
11
+ LocalizedText = tuple[str, str]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class TimeoutProfileSettings:
16
+ default_timeout_ms: int
17
+ profiles: Mapping[str, TimeoutPreset]
18
+ localized_profiles: Mapping[str, Mapping[str, LocalizedText]]
19
+ llm_preset_timeouts: Mapping[str, int]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class TimeoutProfilePorts:
24
+ positive_int: Callable[[Any], int | None]
25
+ pad_cells: Callable[[str, int], str]
26
+ ui_text: Callable[[str, str], str]
27
+ format_minutes: Callable[[int, str], str]
28
+
29
+
30
+ class TimeoutProfileService:
31
+ def __init__(
32
+ self,
33
+ settings: TimeoutProfileSettings,
34
+ ports: TimeoutProfilePorts,
35
+ ) -> None:
36
+ self.settings = settings
37
+ self.ports = ports
38
+
39
+ def llm_preset_timeout(self, preset_id: str) -> int:
40
+ return self.settings.llm_preset_timeouts.get(
41
+ preset_id,
42
+ self.settings.default_timeout_ms,
43
+ )
44
+
45
+ def active_llm_preset_timeout(self, config: dict[str, Any]) -> int | None:
46
+ preset_id = str(config.get("llm_preset") or "").strip()
47
+ if not preset_id:
48
+ return None
49
+ return self.ports.positive_int(
50
+ self.settings.llm_preset_timeouts.get(preset_id)
51
+ )
52
+
53
+ def profile_id(self, milliseconds: int | None) -> str | None:
54
+ if not milliseconds:
55
+ return None
56
+ for profile_id, (profile_ms, _label, _description) in self.settings.profiles.items():
57
+ if milliseconds == profile_ms:
58
+ return profile_id
59
+ return None
60
+
61
+ def text(self, profile_id: str, language: str) -> LocalizedText:
62
+ if profile_id == "__custom__":
63
+ return {
64
+ "ko": ("사용자 지정", "직접 입력한 timeout 값"),
65
+ "ja": ("カスタム", "直接入力した timeout 値"),
66
+ "zh": ("自定义", "手动输入的 timeout 值"),
67
+ }.get(language, ("Custom", "manually entered timeout value"))
68
+ fallback = self.settings.profiles[profile_id]
69
+ return self.settings.localized_profiles.get(language, {}).get(
70
+ profile_id,
71
+ (fallback[1], fallback[2]),
72
+ )
73
+
74
+ def status(self, config: dict[str, Any], language: str) -> str:
75
+ milliseconds = (
76
+ self.ports.positive_int(config.get("request_timeout_ms"))
77
+ or self.settings.default_timeout_ms
78
+ )
79
+ profile_id = self.profile_id(milliseconds)
80
+ label = self.text(profile_id or "__custom__", language)[0]
81
+ idle = self.ports.positive_int(config.get("stream_idle_timeout_ms"))
82
+ idle_text = f"; idle {idle}ms" if idle and idle != milliseconds else ""
83
+ return f"{label}; {milliseconds}ms{idle_text}"
84
+
85
+ @staticmethod
86
+ def idle_timeout(milliseconds: int) -> int:
87
+ return min(milliseconds, 300000)
88
+
89
+ def panel_rows(
90
+ self,
91
+ config: dict[str, Any],
92
+ language: str,
93
+ ) -> tuple[list[str], list[str]]:
94
+ current_ms = (
95
+ self.ports.positive_int(config.get("request_timeout_ms"))
96
+ or self.settings.default_timeout_ms
97
+ )
98
+ rows = [
99
+ f"Current timeout: {current_ms} ms = "
100
+ f"{self.ports.format_minutes(current_ms, language)}"
101
+ ]
102
+ values = ["__info__"]
103
+ current_profile = self.profile_id(current_ms)
104
+ for profile_id, (milliseconds, _label, _description) in self.settings.profiles.items():
105
+ label, description = self.text(profile_id, language)
106
+ mark = "*" if profile_id == current_profile else " "
107
+ rows.append(
108
+ f"{mark} {self.ports.pad_cells(label, 22)} "
109
+ f"{milliseconds:>7} ms {description}"
110
+ )
111
+ values.append(profile_id)
112
+ rows.append(self.ports.ui_text("back", language))
113
+ values.append("back")
114
+ return rows, values
115
+
116
+ def apply(
117
+ self,
118
+ config: dict[str, Any],
119
+ profile_id: str,
120
+ language: str,
121
+ ) -> list[str]:
122
+ if profile_id not in self.settings.profiles:
123
+ raise SystemExit(f"Unknown timeout preset: {profile_id}")
124
+ milliseconds, _label, _description = self.settings.profiles[profile_id]
125
+ idle_ms = self.idle_timeout(milliseconds)
126
+ config["request_timeout_ms"] = milliseconds
127
+ config["stream_idle_timeout_ms"] = idle_ms
128
+ label = self.text(profile_id, language)[0]
129
+ return [
130
+ f"Timeout preset: {label}",
131
+ f"request_timeout_ms: {milliseconds}",
132
+ f"stream_idle_timeout_ms: {idle_ms}",
133
+ ]
134
+
135
+ def with_llm_preset_timeout(
136
+ self,
137
+ tokens: list[str],
138
+ preset_id: str,
139
+ ) -> list[str]:
140
+ filtered = [
141
+ token
142
+ for token in tokens
143
+ if not token.startswith(
144
+ (
145
+ "timeout=",
146
+ "timeout_ms=",
147
+ "request_timeout=",
148
+ "request_timeout_ms=",
149
+ "stream_idle_timeout=",
150
+ "stream_idle_timeout_ms=",
151
+ )
152
+ )
153
+ ]
154
+ timeout_ms = self.llm_preset_timeout(preset_id)
155
+ idle_ms = self.idle_timeout(timeout_ms)
156
+ filtered.append(f"timeout={timeout_ms}")
157
+ filtered.append(f"stream_idle_timeout_ms={idle_ms}")
158
+ return filtered
159
+
160
+
161
+ @dataclass(frozen=True, slots=True)
162
+ class TimeoutProfileApi:
163
+ """Explicit compatibility API with an injected default-language source."""
164
+
165
+ service_factory: Callable[[], TimeoutProfileService]
166
+ default_language: Callable[[], str]
167
+
168
+ def _language(self, language: str | None) -> str:
169
+ return language or self.default_language()
170
+
171
+ def llm_preset_timeout_ms(self, preset_id: str) -> int:
172
+ return self.service_factory().llm_preset_timeout(preset_id)
173
+
174
+ def active_llm_preset_timeout_ms(self, pcfg: dict[str, Any]) -> int | None:
175
+ return self.service_factory().active_llm_preset_timeout(pcfg)
176
+
177
+ def timeout_profile_id_for_ms(self, ms: int | None) -> str | None:
178
+ return self.service_factory().profile_id(ms)
179
+
180
+ def timeout_profile_text(self, profile_id: str, lang: str | None = None) -> LocalizedText:
181
+ return self.service_factory().text(profile_id, self._language(lang))
182
+
183
+ def timeout_profile_status(self, pcfg: dict[str, Any], lang: str | None = None) -> str:
184
+ return self.service_factory().status(pcfg, self._language(lang))
185
+
186
+ def timeout_profile_idle_ms(self, request_timeout_ms: int) -> int:
187
+ return self.service_factory().idle_timeout(request_timeout_ms)
188
+
189
+ def timeout_profile_panel_rows(self, pcfg: dict[str, Any], lang: str | None = None) -> tuple[list[str], list[str]]:
190
+ return self.service_factory().panel_rows(pcfg, self._language(lang))
191
+
192
+ def apply_timeout_profile_to_provider(self, pcfg: dict[str, Any], profile_id: str, lang: str | None = None) -> list[str]:
193
+ return self.service_factory().apply(pcfg, profile_id, self._language(lang))
194
+
195
+ def with_preset_timeout_tokens(self, tokens: list[str], preset_id: str) -> list[str]:
196
+ return self.service_factory().with_llm_preset_timeout(tokens, preset_id)
@@ -0,0 +1,85 @@
1
+ """Runtime-specific tool-name dialects and their registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Mapping
7
+ from typing import Any
8
+
9
+ from .architecture import ToolDialect
10
+ from .registry import AdapterRegistry
11
+
12
+
13
+ def mcp_server_normalized_key(name: str) -> tuple[str, str] | None:
14
+ match = re.fullmatch(r"mcp__(.+)__(.+)", str(name or ""))
15
+ if not match:
16
+ return None
17
+ server, tool = match.groups()
18
+ return re.sub(r"[-_]", "", server).lower(), tool.lower()
19
+
20
+
21
+ def match_available_tool_name(name: str, available: set[str]) -> str | None:
22
+ if not available:
23
+ return None
24
+ if name in available:
25
+ return name
26
+ lowered = str(name or "").lower()
27
+ case_matches = [candidate for candidate in available if candidate.lower() == lowered]
28
+ if len(case_matches) == 1:
29
+ return case_matches[0]
30
+ if not name.startswith("mcp__"):
31
+ normalized = re.sub(r"[^a-z0-9]+", "", lowered)
32
+ matches = [
33
+ candidate
34
+ for candidate in available
35
+ if not candidate.startswith("mcp__")
36
+ and re.sub(r"[^a-z0-9]+", "", candidate.lower()) == normalized
37
+ ]
38
+ if len(matches) == 1:
39
+ return matches[0]
40
+ requested_mcp = mcp_server_normalized_key(name)
41
+ if requested_mcp is not None:
42
+ matches = [candidate for candidate in available if mcp_server_normalized_key(candidate) == requested_mcp]
43
+ if len(matches) == 1:
44
+ return matches[0]
45
+ substring_matches = [
46
+ candidate for candidate in available if lowered and (lowered in candidate.lower() or candidate.lower() in lowered)
47
+ ]
48
+ return sorted(substring_matches)[0] if substring_matches else None
49
+
50
+
51
+ class ClaudeToolDialect(ToolDialect):
52
+ name = "claude"
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ available_tools: set[str] | None = None,
58
+ repair: Callable[[str, dict[str, Any]], Mapping[str, Any]] | None = None,
59
+ blocked: set[str] | None = None,
60
+ ) -> None:
61
+ self._available_tools = available_tools or set()
62
+ self._repair = repair
63
+ self._blocked = frozenset(blocked or ())
64
+
65
+ def normalize_tool_name(self, name: str) -> str:
66
+ return match_available_tool_name(name, self._available_tools) or str(name)
67
+
68
+ def repair_tool_input(self, tool_name: str, value: Mapping[str, Any]) -> Mapping[str, Any]:
69
+ payload = dict(value)
70
+ return self._repair(tool_name, payload) if self._repair else payload
71
+
72
+ def blocked_tools(self) -> frozenset[str]:
73
+ return self._blocked
74
+
75
+
76
+ TOOL_DIALECTS: AdapterRegistry[ToolDialect] = AdapterRegistry()
77
+ TOOL_DIALECTS.register("claude", lambda **kwargs: ClaudeToolDialect(**kwargs), aliases=("claude-code",))
78
+
79
+
80
+ __all__ = [
81
+ "TOOL_DIALECTS",
82
+ "ClaudeToolDialect",
83
+ "match_available_tool_name",
84
+ "mcp_server_normalized_key",
85
+ ]
@@ -0,0 +1,63 @@
1
+ """Policy for removing tools that must not be exposed to an upstream provider."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ToolExposurePorts:
10
+ blocked_tools: Callable[[str, dict[str, Any]], set[str]]
11
+ ultracode_workflow_preferred: Callable[[dict[str, Any]], bool]
12
+ plan_mode_active: Callable[[dict[str, Any]], bool]
13
+ log: Callable[[str, str], None]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ToolExposurePolicy:
18
+ ports: ToolExposurePorts
19
+
20
+ def filter(self, provider: str, provider_config: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
21
+ blocked = set(self.ports.blocked_tools(provider, provider_config))
22
+ dynamic_blocked: set[str] = set()
23
+ if provider != "anthropic" and self.ports.ultracode_workflow_preferred(body) and not self.ports.plan_mode_active(body):
24
+ dynamic_blocked.add("EnterPlanMode")
25
+ blocked.update(dynamic_blocked)
26
+ if not blocked:
27
+ return body
28
+ tools = body.get("tools")
29
+ tool_choice = body.get("tool_choice") if isinstance(body.get("tool_choice"), dict) else None
30
+ tool_choice_name = tool_choice.get("name") if tool_choice else None
31
+ drop_choice = isinstance(tool_choice_name, str) and tool_choice_name in blocked
32
+ if not isinstance(tools, list) or not tools:
33
+ return self._without_blocked_choice(body, provider, tool_choice_name) if drop_choice else body
34
+ kept: list[Any] = []
35
+ dropped: list[str] = []
36
+ for tool in tools:
37
+ name = tool.get("name") if isinstance(tool, dict) else None
38
+ if isinstance(name, str) and name in blocked:
39
+ dropped.append(name)
40
+ else:
41
+ kept.append(tool)
42
+ if not dropped:
43
+ return self._without_blocked_choice(body, provider, tool_choice_name) if drop_choice else body
44
+ reason = " ultracode_workflow_preferred=true" if dynamic_blocked.intersection(dropped) else ""
45
+ self.ports.log("INFO", f"filtered upstream tools for {provider}: {', '.join(sorted(set(dropped)))}{reason}")
46
+ filtered = dict(body)
47
+ filtered["tools"] = kept
48
+ if drop_choice:
49
+ filtered.pop("tool_choice", None)
50
+ self._log_removed_choice(provider, tool_choice_name)
51
+ return filtered
52
+
53
+ def _without_blocked_choice(self, body: dict[str, Any], provider: str, tool_choice_name: Any) -> dict[str, Any]:
54
+ filtered = dict(body)
55
+ filtered.pop("tool_choice", None)
56
+ self._log_removed_choice(provider, tool_choice_name)
57
+ return filtered
58
+
59
+ def _log_removed_choice(self, provider: str, tool_choice_name: Any) -> None:
60
+ self.ports.log("WARN", f"removed blocked tool_choice for {provider}: {tool_choice_name}")
61
+
62
+
63
+ __all__ = ["ToolExposurePolicy", "ToolExposurePorts"]