@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,193 @@
1
+ """Application clients for Advisor review and refinement provider calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class AdvisorClientPolicy:
11
+ model_enabled: Callable[[dict[str, Any]], str]
12
+ provider_supported: Callable[[str], bool]
13
+ upstream_model: Callable[[str, str], str]
14
+ provider_kind: Callable[..., str]
15
+ request: Callable[..., dict[str, Any]]
16
+ endpoint: Callable[[str, dict[str, Any]], str]
17
+ response_text: Callable[[str, Any], str]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class AdvisorClientIO:
22
+ apply_rate_limit: Callable[..., None]
23
+ write_activity: Callable[..., None]
24
+ estimate_tokens: Callable[[Any], int]
25
+ post_json: Callable[..., Any]
26
+ headers: Callable[..., dict[str, str]]
27
+ provider_timeout: Callable[[dict[str, Any]], float]
28
+ ollama_timeout: Callable[[dict[str, Any]], float]
29
+ log: Callable[[str, str], None]
30
+
31
+
32
+ class AdvisorClient:
33
+ def __init__(self, policy: AdvisorClientPolicy, io: AdvisorClientIO) -> None:
34
+ self.policy = policy
35
+ self.io = io
36
+
37
+ def review(
38
+ self,
39
+ provider: str,
40
+ config: dict[str, Any],
41
+ body: dict[str, Any],
42
+ focus: str = "",
43
+ inbound_headers: Any | None = None,
44
+ *,
45
+ allow_rate_limit_wait: bool = True,
46
+ retry_rate_limits: bool = True,
47
+ raise_errors: bool = False,
48
+ ) -> str:
49
+ advisor_model = self.policy.model_enabled(config)
50
+ if not advisor_model or not self.policy.provider_supported(provider):
51
+ return ""
52
+ upstream_model = self.policy.upstream_model(provider, advisor_model)
53
+ request_body = self.policy.request(
54
+ provider, advisor_model, body, config, focus_override=focus
55
+ )
56
+ try:
57
+ if allow_rate_limit_wait:
58
+ self.io.apply_rate_limit(provider, config, upstream_model)
59
+ self.io.write_activity(
60
+ "advisor",
61
+ provider,
62
+ upstream_model,
63
+ tokens=self.io.estimate_tokens(request_body),
64
+ )
65
+ kind = self.policy.provider_kind(provider)
66
+ headers = (
67
+ self.io.headers(
68
+ provider,
69
+ config,
70
+ inbound_headers,
71
+ "anthropic_messages",
72
+ )
73
+ if kind == "anthropic"
74
+ else self.io.headers(provider, config)
75
+ )
76
+ timeout = (
77
+ self.io.ollama_timeout(config)
78
+ if kind == "ollama"
79
+ else self.io.provider_timeout(config)
80
+ )
81
+ data = self.io.post_json(
82
+ self.policy.endpoint(provider, config),
83
+ request_body,
84
+ headers,
85
+ timeout,
86
+ provider,
87
+ config,
88
+ upstream_model,
89
+ None,
90
+ retry_rate_limits=retry_rate_limits,
91
+ )
92
+ text = self.policy.response_text(provider, data)
93
+ if text:
94
+ self.io.log(
95
+ "INFO",
96
+ f"advisor_feedback provider={provider} "
97
+ f"advisor_model={upstream_model} chars={len(text)}",
98
+ )
99
+ return text
100
+ except Exception as exc:
101
+ self.io.log(
102
+ "WARN",
103
+ f"advisor_request_failed provider={provider} "
104
+ f"advisor_model={upstream_model} "
105
+ f"error={type(exc).__name__}: {exc}",
106
+ )
107
+ self.io.write_activity(
108
+ "advisor_error",
109
+ provider,
110
+ upstream_model,
111
+ error=type(exc).__name__,
112
+ )
113
+ if raise_errors:
114
+ raise
115
+ return ""
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class ProviderChatPolicy:
120
+ normalize_thinking: Callable[..., dict[str, Any]]
121
+ normalize_tool_choice: Callable[..., dict[str, Any]]
122
+ provider_kind: Callable[..., str]
123
+ upstream_model: Callable[[str, dict[str, Any], str], str]
124
+ ollama_request: Callable[..., dict[str, Any]]
125
+ openai_request: Callable[..., dict[str, Any]]
126
+ ollama_response: Callable[..., dict[str, Any]]
127
+ openai_response: Callable[..., dict[str, Any]]
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class ProviderChatIO:
132
+ apply_rate_limit: Callable[..., None]
133
+ post_json: Callable[..., Any]
134
+ join_url: Callable[[str, str], str]
135
+ request_base: Callable[[str, dict[str, Any]], str]
136
+ headers: Callable[..., dict[str, str]]
137
+ provider_timeout: Callable[[dict[str, Any]], float]
138
+ ollama_timeout: Callable[[dict[str, Any]], float]
139
+
140
+
141
+ class ProviderChatExecutor:
142
+ def __init__(self, policy: ProviderChatPolicy, io: ProviderChatIO) -> None:
143
+ self.policy = policy
144
+ self.io = io
145
+
146
+ def execute(
147
+ self,
148
+ provider: str,
149
+ config: dict[str, Any],
150
+ body: dict[str, Any],
151
+ model: str,
152
+ ) -> dict[str, Any]:
153
+ body = self.policy.normalize_thinking(provider, config, body)
154
+ body = self.policy.normalize_tool_choice(provider, config, body)
155
+ transport = self.policy.provider_kind(provider, config)
156
+ if transport == "ollama":
157
+ request = self.policy.ollama_request(
158
+ model, body, config, stream=False, provider=provider
159
+ )
160
+ self.io.apply_rate_limit(provider, config, model)
161
+ data = self.io.post_json(
162
+ self.io.join_url(str(config.get("base_url") or "").rstrip("/"), "/api/chat"),
163
+ request,
164
+ self.io.headers(provider, config),
165
+ self.io.ollama_timeout(config),
166
+ provider,
167
+ config,
168
+ model,
169
+ None,
170
+ )
171
+ return self.policy.ollama_response(data, model, source_body=body)
172
+ if transport == "openai-compatible":
173
+ upstream_model = self.policy.upstream_model(provider, config, model)
174
+ request = self.policy.openai_request(
175
+ provider, upstream_model, body, config, stream=False
176
+ )
177
+ self.io.apply_rate_limit(provider, config, upstream_model)
178
+ data = self.io.post_json(
179
+ self.io.join_url(
180
+ self.io.request_base(provider, config), "/v1/chat/completions"
181
+ ),
182
+ request,
183
+ self.io.headers(provider, config),
184
+ self.io.provider_timeout(config),
185
+ provider,
186
+ config,
187
+ upstream_model,
188
+ None,
189
+ )
190
+ return self.policy.openai_response(data, upstream_model, source_body=body)
191
+ raise RuntimeError(
192
+ f"Advisor refinement is not supported for provider {provider}"
193
+ )
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class AdvisorTextServices:
9
+ content_to_text: Callable[[Any], str]
10
+ tool_input_for_prompt: Callable[[Any], str]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AdvisorDecisionServices:
15
+ advisor_enabled: Callable[[dict[str, Any]], bool]
16
+ is_advisor_request: Callable[[dict[str, Any]], bool]
17
+ completed_work: Callable[[dict[str, Any]], bool]
18
+ non_actionable_response: Callable[[str], bool]
19
+ plan_mode_active: Callable[[dict[str, Any]], bool]
20
+ provider_supported: Callable[[str], bool]
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AdvisorServices:
25
+ text: AdvisorTextServices
26
+ decisions: AdvisorDecisionServices
27
+ log: Callable[[str, str], None]
28
+ feedback_marker: str
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class AdvisorShortcutPorts:
33
+ should_intercept: Callable[[str, dict[str, Any]], bool]
34
+ is_request: Callable[[dict[str, Any]], bool]
35
+ provider_supported: Callable[[str], bool]
36
+ call_text: Callable[..., str]
37
+ write_anthropic: Callable[..., Any]
38
+ load_config: Callable[[], dict[str, Any]]
39
+ current_alias: Callable[[dict[str, Any]], str]
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class AdvisorShortcutController:
44
+ ports: AdvisorShortcutPorts
45
+
46
+ def handle(
47
+ self,
48
+ handler: Any,
49
+ provider: str,
50
+ provider_config: dict[str, Any],
51
+ body: dict[str, Any],
52
+ ) -> bool:
53
+ if not self.ports.should_intercept(provider, provider_config):
54
+ return False
55
+ if not self.ports.is_request(body):
56
+ return False
57
+ advisor_model = str(provider_config.get("advisor_model") or "").strip()
58
+ stream = bool(body.get("stream", True))
59
+ if not advisor_model:
60
+ model = str(
61
+ body.get("model")
62
+ or self.ports.current_alias(self.ports.load_config())
63
+ )
64
+ self.ports.write_anthropic(
65
+ handler,
66
+ model,
67
+ "Advisor is off. Choose an Advisor Model in the ciel-runtime launch menu (item 5), or run `ciel-runtime advisor-model <model-id>`, then use `/advisor` again.",
68
+ stream,
69
+ )
70
+ return True
71
+ if not self.ports.provider_supported(provider):
72
+ self.ports.write_anthropic(
73
+ handler,
74
+ advisor_model,
75
+ f"Advisor Model is configured as `{advisor_model}`, but ciel-runtime advisor calling is not implemented for provider `{provider}`.",
76
+ stream,
77
+ )
78
+ return True
79
+ try:
80
+ text = self.ports.call_text(
81
+ provider,
82
+ provider_config,
83
+ body,
84
+ inbound_headers=handler.headers,
85
+ allow_rate_limit_wait=False,
86
+ retry_rate_limits=False,
87
+ raise_errors=True,
88
+ )
89
+ if not text:
90
+ text = "Advisor returned no text."
91
+ except Exception as exc:
92
+ text = f"Advisor request failed: {type(exc).__name__}: {exc}"
93
+ self.ports.write_anthropic(
94
+ handler, advisor_model, "Advisor guidance:\n\n" + text, stream
95
+ )
96
+ return True
97
+
98
+
99
+ def advisor_messages_and_system(
100
+ body: dict[str, Any], services: AdvisorServices
101
+ ) -> tuple[list[dict[str, Any]], list[str]]:
102
+ messages: list[dict[str, Any]] = []
103
+ system_texts: list[str] = []
104
+ for message in body.get("messages", []) or []:
105
+ if not isinstance(message, dict):
106
+ continue
107
+ role = str(message.get("role") or "").strip()
108
+ text = services.text.content_to_text(message.get("content")).strip()
109
+ if role == "system":
110
+ if text:
111
+ system_texts.append(text)
112
+ elif role in {"user", "assistant"}:
113
+ messages.append(message)
114
+ elif text:
115
+ messages.append(
116
+ {
117
+ "role": "user",
118
+ "content": [{"type": "text", "text": f"[{role or 'unknown'} message]\n{text}"}],
119
+ }
120
+ )
121
+ return messages, system_texts
122
+
123
+
124
+ def advisor_tool_schema() -> dict[str, Any]:
125
+ return {
126
+ "name": "advisor",
127
+ "description": (
128
+ "Consult a second, stronger advisor model for an independent review before you make an important "
129
+ "plan, architecture, debugging, or final-completion decision. Use this when additional scrutiny could "
130
+ "catch gaps, risks, or better next steps."
131
+ ),
132
+ "input_schema": {
133
+ "type": "object",
134
+ "properties": {
135
+ "question": {
136
+ "type": "string",
137
+ "description": "The specific decision, plan, code path, or risk you want the advisor to review.",
138
+ }
139
+ },
140
+ "required": ["question"],
141
+ },
142
+ }
143
+
144
+
145
+ def body_with_advisor_tool(
146
+ body: dict[str, Any], provider_config: dict[str, Any], services: AdvisorServices
147
+ ) -> dict[str, Any]:
148
+ if not services.decisions.advisor_enabled(provider_config):
149
+ return body
150
+ tools = body.get("tools") if isinstance(body.get("tools"), list) else []
151
+ if any(isinstance(tool, dict) and str(tool.get("name") or "") == "advisor" for tool in tools):
152
+ return body
153
+ output = dict(body)
154
+ output["tools"] = [*tools, advisor_tool_schema()]
155
+ return output
156
+
157
+
158
+ def is_claude_code_advisor_server_tool(tool: Any) -> bool:
159
+ return bool(
160
+ isinstance(tool, dict)
161
+ and str(tool.get("name") or "") == "advisor"
162
+ and str(tool.get("type") or "").startswith("advisor_")
163
+ )
164
+
165
+
166
+ def strip_autonomous_advisor_server_tools(
167
+ body: dict[str, Any],
168
+ services: AdvisorServices,
169
+ *,
170
+ server_tool_supported: bool,
171
+ ) -> dict[str, Any]:
172
+ if server_tool_supported or services.decisions.is_advisor_request(body):
173
+ return body
174
+ if body_has_advisor_feedback(body, services):
175
+ return body
176
+ tools = body.get("tools")
177
+ if not isinstance(tools, list) or not tools:
178
+ return body
179
+ kept = [tool for tool in tools if not is_claude_code_advisor_server_tool(tool)]
180
+ removed = len(tools) - len(kept)
181
+ if not removed:
182
+ return body
183
+ output = dict(body)
184
+ output["tools"] = kept
185
+ services.log("INFO", f"stripped autonomous advisor server tool count={removed}")
186
+ return output
187
+
188
+
189
+ def advisor_tool_focus_from_message(message: dict[str, Any]) -> str | None:
190
+ content = message.get("content")
191
+ if not isinstance(content, list):
192
+ return None
193
+ for block in content:
194
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
195
+ continue
196
+ if str(block.get("name") or "") != "advisor":
197
+ continue
198
+ tool_input = block.get("input")
199
+ if isinstance(tool_input, dict):
200
+ for key in ("question", "prompt", "focus", "task"):
201
+ value = tool_input.get(key)
202
+ if isinstance(value, str) and value.strip():
203
+ return value.strip()
204
+ return "advisor tool call"
205
+ return None
206
+
207
+
208
+ def tool_review_context_from_message(
209
+ message: dict[str, Any], trigger: str, services: AdvisorServices
210
+ ) -> str:
211
+ content = message.get("content")
212
+ if not isinstance(content, list):
213
+ return ""
214
+ sections: list[str] = []
215
+ for block in content:
216
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
217
+ continue
218
+ name = str(block.get("name") or "").strip()
219
+ if not name or name == "advisor":
220
+ continue
221
+ input_text = services.text.tool_input_for_prompt(block.get("input"))
222
+ if name == "ExitPlanMode":
223
+ sections.append(
224
+ "Review target: ExitPlanMode plan before user approval.\n"
225
+ "The executor is about to submit this plan. Review the actual plan/tool input below.\n"
226
+ f"Tool input:\n{input_text}"
227
+ )
228
+ elif trigger:
229
+ sections.append(f"Review target tool: {name} ({trigger}).\nTool input:\n{input_text}")
230
+ return "\n\n".join(sections)
231
+
232
+
233
+ def advisor_focus_for_message(
234
+ message: dict[str, Any], trigger: str | None, services: AdvisorServices
235
+ ) -> tuple[str | None, str | None]:
236
+ explicit_focus = advisor_tool_focus_from_message(message)
237
+ if explicit_focus:
238
+ return "advisor tool call", explicit_focus
239
+ if not trigger:
240
+ return None, None
241
+ review_context = tool_review_context_from_message(message, trigger, services)
242
+ return trigger, review_context or trigger
243
+
244
+
245
+ def assistant_tool_call_summary_for_prompt(
246
+ message: dict[str, Any], services: AdvisorServices
247
+ ) -> str:
248
+ content = message.get("content")
249
+ if not isinstance(content, list):
250
+ return ""
251
+ sections: list[str] = []
252
+ for block in content:
253
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
254
+ continue
255
+ name = str(block.get("name") or "tool").strip() or "tool"
256
+ if name != "advisor":
257
+ sections.append(
258
+ f"Pending Claude Code tool call: {name}\nTool input:\n"
259
+ f"{services.text.tool_input_for_prompt(block.get('input'))}"
260
+ )
261
+ return "\n\n".join(sections)
262
+
263
+
264
+ def body_has_advisor_feedback(body: dict[str, Any], services: AdvisorServices) -> bool:
265
+ for message in reversed(body.get("messages") or []):
266
+ if not isinstance(message, dict):
267
+ continue
268
+ if services.feedback_marker in services.text.content_to_text(message.get("content")):
269
+ return True
270
+ return False
271
+
272
+
273
+ def anthropic_message_tool_names(message: dict[str, Any]) -> list[str]:
274
+ content = message.get("content")
275
+ if not isinstance(content, list):
276
+ return []
277
+ names: list[str] = []
278
+ for block in content:
279
+ if isinstance(block, dict) and block.get("type") == "tool_use":
280
+ name = block.get("name")
281
+ if isinstance(name, str) and name:
282
+ names.append(name)
283
+ return names
284
+
285
+
286
+ def advisor_trigger_for_message(
287
+ body: dict[str, Any], message: dict[str, Any], services: AdvisorServices
288
+ ) -> str | None:
289
+ names = set(anthropic_message_tool_names(message))
290
+ if "ExitPlanMode" in names:
291
+ return "before ExitPlanMode plan approval"
292
+ if names:
293
+ return None
294
+ text = services.text.content_to_text(message.get("content")).strip()
295
+ if (
296
+ text
297
+ and message.get("stop_reason") == "end_turn"
298
+ and services.decisions.completed_work(body)
299
+ and not services.decisions.non_actionable_response(text)
300
+ ):
301
+ return "before completion/final response"
302
+ return None
303
+
304
+
305
+ def advisor_gate_reason_for_body(
306
+ provider: str,
307
+ provider_config: dict[str, Any],
308
+ body: dict[str, Any],
309
+ services: AdvisorServices,
310
+ ) -> str:
311
+ decisions = services.decisions
312
+ if not decisions.advisor_enabled(provider_config) or not decisions.provider_supported(provider):
313
+ return ""
314
+ if decisions.is_advisor_request(body) or body_has_advisor_feedback(body, services):
315
+ return ""
316
+ if decisions.plan_mode_active(body):
317
+ return "plan_mode"
318
+ if decisions.completed_work(body):
319
+ return "completed_work"
320
+ return ""
@@ -0,0 +1,160 @@
1
+ """Apply Advisor feedback as a bounded refinement decorator around a response."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class AdvisorRefinementText:
13
+ content_text: Callable[[Any], str]
14
+ tool_names: Callable[[dict[str, Any]], list[str]]
15
+ tool_summary: Callable[[dict[str, Any]], str]
16
+ compact_text: Callable[[str], str]
17
+ prepend_text: Callable[[dict[str, Any], str], dict[str, Any]]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class AdvisorRefinementPolicy:
22
+ model_enabled: Callable[[dict[str, Any]], bool]
23
+ provider_supported: Callable[[str], bool]
24
+ is_advisor_request: Callable[[dict[str, Any]], bool]
25
+ has_feedback: Callable[[dict[str, Any]], bool]
26
+ trigger: Callable[[dict[str, Any], dict[str, Any]], str]
27
+ focus: Callable[[dict[str, Any], str], tuple[str, str]]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class AdvisorRefinementIO:
32
+ call_advisor: Callable[..., str]
33
+ call_provider: Callable[..., dict[str, Any]]
34
+ log: Callable[[str, str], None]
35
+ write_activity: Callable[..., None]
36
+
37
+
38
+ class AdvisorRefinementService:
39
+ def __init__(
40
+ self,
41
+ feedback_marker: str,
42
+ text: AdvisorRefinementText,
43
+ policy: AdvisorRefinementPolicy,
44
+ io: AdvisorRefinementIO,
45
+ ) -> None:
46
+ self.feedback_marker = feedback_marker
47
+ self.text = text
48
+ self.policy = policy
49
+ self.io = io
50
+
51
+ def body_with_feedback(
52
+ self,
53
+ body: dict[str, Any],
54
+ assistant_message: dict[str, Any],
55
+ advisor_text: str,
56
+ trigger: str,
57
+ ) -> dict[str, Any]:
58
+ messages = [item for item in body.get("messages", []) if isinstance(item, dict)]
59
+ assistant_text = self.text.content_text(assistant_message.get("content")).strip()
60
+ tool_names = self.text.tool_names(assistant_message)
61
+ tool_summary = self.text.tool_summary(assistant_message)
62
+ if assistant_text:
63
+ summary = self.text.compact_text(assistant_text)
64
+ elif tool_summary:
65
+ summary = self.text.compact_text(tool_summary)
66
+ elif tool_names:
67
+ summary = (
68
+ "I was about to call Claude Code tool(s): "
69
+ + ", ".join(tool_names)
70
+ + "."
71
+ )
72
+ else:
73
+ summary = ""
74
+ if summary:
75
+ messages.append(
76
+ {"role": "assistant", "content": [{"type": "text", "text": summary}]}
77
+ )
78
+ feedback = (
79
+ f"{self.feedback_marker}\n"
80
+ f"Advisor review trigger: {trigger}.\n\n"
81
+ f"{advisor_text}\n\n"
82
+ "Apply this advisor feedback now. If the plan is still ready, call "
83
+ "ExitPlanMode with the improved plan. If the task is complete, provide "
84
+ "the final answer. If the advisor found a gap, continue with concrete "
85
+ "Claude Code tool calls instead of stopping after an announcement."
86
+ )
87
+ messages.append(
88
+ {"role": "user", "content": [{"type": "text", "text": feedback}]}
89
+ )
90
+ projected = dict(body)
91
+ projected["messages"] = messages
92
+ projected.pop("tool_choice", None)
93
+ return projected
94
+
95
+ @staticmethod
96
+ def visible_summary(advisor_text: str, trigger: str, limit: int = 700) -> str:
97
+ text = re.sub(r"\s+", " ", str(advisor_text or "")).strip()
98
+ if not text:
99
+ return ""
100
+ if len(text) > limit:
101
+ text = text[: max(0, limit - 1)].rstrip() + "…"
102
+ return f"Advisor review ({trigger}): {text}\n\n"
103
+
104
+ def refine(
105
+ self,
106
+ provider: str,
107
+ config: dict[str, Any],
108
+ original_body: dict[str, Any],
109
+ message: dict[str, Any],
110
+ main_model: str,
111
+ ) -> dict[str, Any]:
112
+ if not self.policy.model_enabled(config):
113
+ return message
114
+ if not self.policy.provider_supported(provider):
115
+ return message
116
+ if self.policy.is_advisor_request(original_body) or self.policy.has_feedback(
117
+ original_body
118
+ ):
119
+ return message
120
+ trigger = self.policy.trigger(original_body, message)
121
+ trigger, advisor_focus = self.policy.focus(message, trigger)
122
+ if not trigger:
123
+ return message
124
+ advisor_text = self.io.call_advisor(
125
+ provider,
126
+ config,
127
+ original_body,
128
+ focus=advisor_focus or trigger,
129
+ )
130
+ if not advisor_text:
131
+ return message
132
+ follow_body = self.body_with_feedback(
133
+ original_body, message, advisor_text, trigger
134
+ )
135
+ summary = self.visible_summary(advisor_text, trigger)
136
+ try:
137
+ self.io.log(
138
+ "INFO",
139
+ f"advisor_refinement_call trigger={trigger} main_model={main_model}",
140
+ )
141
+ refined = self.io.call_provider(provider, config, follow_body, main_model)
142
+ self.io.log(
143
+ "INFO",
144
+ f"advisor_refinement_done trigger={trigger} "
145
+ f"stop_reason={refined.get('stop_reason')}",
146
+ )
147
+ return self.text.prepend_text(refined, summary)
148
+ except Exception as exc:
149
+ self.io.log(
150
+ "WARN",
151
+ f"advisor_refinement_failed trigger={trigger} model={main_model} "
152
+ f"error={type(exc).__name__}: {exc}",
153
+ )
154
+ self.io.write_activity(
155
+ "advisor_refinement_error",
156
+ provider,
157
+ main_model,
158
+ error=type(exc).__name__,
159
+ )
160
+ return self.text.prepend_text(message, summary)