@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,261 @@
1
+ """Build and decode provider-specific Advisor requests without performing I/O."""
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
+ ADVISOR_REVIEW_PROMPT = (
11
+ "You are ciel-runtime Advisor, a stronger reviewer model. Review the current task state and provide "
12
+ "concise, actionable guidance for the executor model. Review now; do not say that you will review later. "
13
+ "Do not write code unless a small exact patch is the clearest advice. Use this exact structure: "
14
+ "Verdict: approve, revise, or continue. Key findings: concrete gaps or risks. Required next action: "
15
+ "the next action or Claude Code tool call. Validation: the check that proves the work. "
16
+ "If the executor is stuck after progress announcements, tell it the exact next Claude Code tool to call."
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class AdvisorProjectionPorts:
22
+ provider_kind: Callable[..., str]
23
+ anthropic_messages: Callable[..., tuple[list[dict[str, Any]], list[str]]]
24
+ openai_messages: Callable[..., list[dict[str, Any]]]
25
+ ollama_messages: Callable[[dict[str, Any]], list[dict[str, Any]]]
26
+ focus_from_body: Callable[[dict[str, Any]], str]
27
+ compact_text: Callable[[str], str]
28
+ anthropic_system: Callable[[Any, list[str]], Any]
29
+ anthropic_text: Callable[[Any], str]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class AdvisorBudgetPorts:
34
+ ollama_context: Callable[[dict[str, Any]], int]
35
+ provider_context: Callable[[str, dict[str, Any]], int]
36
+ openai_context: Callable[[str, dict[str, Any]], int]
37
+ reserve: Callable[[dict[str, Any], int], int]
38
+ compact_messages: Callable[..., list[dict[str, Any]]]
39
+ configured_output: Callable[..., int]
40
+ ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
41
+ positive_int: Callable[[Any], int]
42
+ ollama_num_ctx: Callable[..., int]
43
+ think_enabled: Callable[[str | None, dict[str, Any]], bool]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class AdvisorEndpointPorts:
48
+ join_url: Callable[[str, str], str]
49
+ upstream_query: Callable[[dict[str, Any], str, str], str]
50
+ provider_request_base: Callable[[str, dict[str, Any]], str]
51
+ upstream_model: Callable[[str], str]
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class AdvisorAnthropicSystemPolicy:
56
+ review_prompt: str
57
+ content_to_text: Callable[[Any], str]
58
+
59
+ def project(
60
+ self,
61
+ system: Any,
62
+ extra_system_texts: list[str] | None = None,
63
+ ) -> list[dict[str, Any]]:
64
+ blocks: list[dict[str, Any]] = []
65
+ original_text = ""
66
+ if isinstance(system, str):
67
+ clean = system.strip()
68
+ if clean:
69
+ blocks.append({"type": "text", "text": clean})
70
+ elif isinstance(system, list) and system:
71
+ first = system[0]
72
+ if (
73
+ isinstance(first, dict)
74
+ and str(first.get("type") or "") == "text"
75
+ and str(first.get("text") or "").strip()
76
+ ):
77
+ blocks.append(dict(first))
78
+ original_text = self.content_to_text(system[1:]).strip()
79
+ else:
80
+ original_text = self.content_to_text(system).strip()
81
+ elif system:
82
+ original_text = self.content_to_text(system).strip()
83
+ blocks.append({"type": "text", "text": self.review_prompt})
84
+ if original_text:
85
+ blocks.append(
86
+ {
87
+ "type": "text",
88
+ "text": (
89
+ "Original session system context:\n"
90
+ + original_text
91
+ ),
92
+ }
93
+ )
94
+ for text in extra_system_texts or []:
95
+ clean = str(text or "").strip()
96
+ if clean:
97
+ blocks.append(
98
+ {
99
+ "type": "text",
100
+ "text": (
101
+ "Additional system context from message "
102
+ "history:\n" + clean
103
+ ),
104
+ }
105
+ )
106
+ return blocks
107
+
108
+
109
+ class AdvisorRequestBuilder:
110
+ def __init__(
111
+ self,
112
+ review_prompt: str,
113
+ projection: AdvisorProjectionPorts,
114
+ budget: AdvisorBudgetPorts,
115
+ endpoint: AdvisorEndpointPorts,
116
+ ) -> None:
117
+ self.review_prompt = review_prompt
118
+ self.projection = projection
119
+ self.budget = budget
120
+ self.endpoint = endpoint
121
+
122
+ def messages(
123
+ self, provider: str, body: dict[str, Any], focus_override: str = ""
124
+ ) -> list[dict[str, Any]]:
125
+ kind = self.projection.provider_kind(provider)
126
+ if kind == "anthropic":
127
+ messages, _ = self.projection.anthropic_messages(body)
128
+ elif kind == "openai-compatible":
129
+ messages = self.projection.openai_messages(body)
130
+ else:
131
+ messages = self.projection.ollama_messages(body)
132
+ focus = focus_override or self.projection.focus_from_body(body)
133
+ if kind != "anthropic":
134
+ messages.insert(0, {"role": "system", "content": self.review_prompt})
135
+ if focus:
136
+ focus_text = f"Advisor focus:\n{self.projection.compact_text(focus)}"
137
+ content: Any = (
138
+ [{"type": "text", "text": focus_text}]
139
+ if kind == "anthropic"
140
+ else focus_text
141
+ )
142
+ messages.append({"role": "user", "content": content})
143
+ return messages
144
+
145
+ def input_budget(self, provider: str, config: dict[str, Any]) -> int:
146
+ kind = self.projection.provider_kind(provider)
147
+ if kind == "ollama":
148
+ context_limit = self.budget.ollama_context(config)
149
+ elif kind == "anthropic":
150
+ context_limit = self.budget.provider_context(provider, config) or 200000
151
+ else:
152
+ context_limit = self.budget.openai_context(provider, config)
153
+ return max(
154
+ 8192,
155
+ context_limit - 4096 - self.budget.reserve(config, context_limit),
156
+ )
157
+
158
+ def model(self, provider: str, model: str) -> str:
159
+ return self.endpoint.upstream_model(model) if provider == "nvidia-hosted" else model
160
+
161
+ def request(
162
+ self,
163
+ provider: str,
164
+ model: str,
165
+ body: dict[str, Any],
166
+ config: dict[str, Any],
167
+ focus_override: str = "",
168
+ ) -> dict[str, Any]:
169
+ kind = self.projection.provider_kind(provider)
170
+ messages = self.messages(provider, body, focus_override)
171
+ if kind != "anthropic":
172
+ messages = self.budget.compact_messages(
173
+ messages,
174
+ [],
175
+ self.input_budget(provider, config),
176
+ provider=provider,
177
+ model=model,
178
+ )
179
+ upstream_model = self.model(provider, model)
180
+ if kind == "anthropic":
181
+ _, extra_system = self.projection.anthropic_messages(body)
182
+ request = {
183
+ "model": upstream_model,
184
+ "system": self.projection.anthropic_system(
185
+ body.get("system"), extra_system
186
+ ),
187
+ "messages": messages,
188
+ "stream": False,
189
+ "max_tokens": min(
190
+ 4096, self.budget.configured_output(config, body) or 4096
191
+ ),
192
+ }
193
+ return self._sampling_options(request, config)
194
+ if kind == "openai-compatible":
195
+ request = {
196
+ "model": upstream_model,
197
+ "messages": messages,
198
+ "stream": False,
199
+ "max_tokens": min(
200
+ 4096, self.budget.configured_output(config, body) or 4096
201
+ ),
202
+ }
203
+ return self._sampling_options(request, config)
204
+ request = {
205
+ "model": upstream_model,
206
+ "messages": messages,
207
+ "stream": False,
208
+ "think": self.budget.think_enabled(upstream_model, config),
209
+ }
210
+ options = self.budget.ollama_options(config)
211
+ options.setdefault(
212
+ "num_predict",
213
+ min(4096, self.budget.positive_int(options.get("num_predict")) or 4096),
214
+ )
215
+ num_ctx = self.budget.ollama_num_ctx(
216
+ config, {"messages": messages, "tools": []}
217
+ )
218
+ if num_ctx:
219
+ options.setdefault("num_ctx", num_ctx)
220
+ if options:
221
+ request["options"] = options
222
+ return request
223
+
224
+ @staticmethod
225
+ def _sampling_options(
226
+ request: dict[str, Any], config: dict[str, Any]
227
+ ) -> dict[str, Any]:
228
+ for key in ("temperature", "top_p"):
229
+ if config.get(key) is not None:
230
+ request[key] = config[key]
231
+ return request
232
+
233
+ def response_text(self, provider: str, data: Any) -> str:
234
+ if not isinstance(data, dict):
235
+ return ""
236
+ kind = self.projection.provider_kind(provider)
237
+ if kind == "anthropic":
238
+ return self.projection.anthropic_text(data.get("content")).strip()
239
+ if kind == "openai-compatible":
240
+ choices = data.get("choices")
241
+ if isinstance(choices, list) and choices:
242
+ message = choices[0].get("message") if isinstance(choices[0], dict) else {}
243
+ if isinstance(message, dict):
244
+ return str(message.get("content") or "").strip()
245
+ return ""
246
+ message = data.get("message")
247
+ return str((message or {}).get("content") or "").strip()
248
+
249
+ def endpoint_url(self, provider: str, config: dict[str, Any]) -> str:
250
+ base = str(config.get("base_url") or "").rstrip("/")
251
+ kind = self.projection.provider_kind(provider)
252
+ if kind == "anthropic":
253
+ url = self.endpoint.join_url(base, "/v1/messages")
254
+ query = self.endpoint.upstream_query(config, "", provider)
255
+ return f"{url}?{query}" if query else url
256
+ if kind == "openai-compatible":
257
+ return self.endpoint.join_url(
258
+ self.endpoint.provider_request_base(provider, config),
259
+ "/v1/chat/completions",
260
+ )
261
+ return self.endpoint.join_url(base, "/api/chat")
@@ -0,0 +1,169 @@
1
+ """Official manifest-backed AGY installation and update lifecycle."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import platform
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tarfile
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any, Callable
17
+ import urllib.request
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class AgyInstallerPorts:
22
+ user_bin_dir: Callable[[], Path]
23
+ forced_environment: Callable[[], dict[str, str]]
24
+ find_executable: Callable[[str], str | None]
25
+ version_newer: Callable[[str, str], bool]
26
+ run_upgrade: Callable[..., tuple[int, str]]
27
+ output: Callable[..., None] = print
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class AgyInstaller:
32
+ manifest_base_url: str
33
+ ports: AgyInstallerPorts
34
+
35
+ def manifest_name(self) -> str:
36
+ arch = "arm64" if platform.machine().lower() in ("arm64", "aarch64") else "amd64"
37
+ platform_name = "windows" if os.name == "nt" else ("darwin" if sys.platform == "darwin" else "linux")
38
+ return f"{platform_name}_{arch}.json"
39
+
40
+ def manifest_url(self) -> str:
41
+ override = str(os.environ.get("CIEL_RUNTIME_AGY_MANIFEST_URL") or "").strip()
42
+ return override or f"{self.manifest_base_url}/manifests/{self.manifest_name()}"
43
+
44
+ @staticmethod
45
+ def download_file(url: str, target: Path, timeout: float = 120.0) -> None:
46
+ with urllib.request.urlopen(url, timeout=timeout) as response, target.open("wb") as output:
47
+ shutil.copyfileobj(response, output)
48
+
49
+ def latest_manifest(self, timeout: float = 15.0) -> dict[str, Any] | None:
50
+ try:
51
+ with urllib.request.urlopen(self.manifest_url(), timeout=timeout) as response:
52
+ data = json.loads(response.read(65536).decode("utf-8", errors="replace"))
53
+ if isinstance(data, dict) and data.get("url") and data.get("version"):
54
+ return data
55
+ except Exception as exc:
56
+ self._print(f"AGY manifest check failed ({type(exc).__name__}); continuing.")
57
+ return None
58
+
59
+ @staticmethod
60
+ def current_version(executable: str) -> str:
61
+ try:
62
+ process = subprocess.run([executable, "--version"], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=8)
63
+ except Exception:
64
+ return ""
65
+ if process.returncode != 0:
66
+ return ""
67
+ match = re.search(r"\d+(?:\.\d+)+(?:[-+][A-Za-z0-9_.-]+)?", process.stdout or "")
68
+ return match.group(0) if match else (process.stdout or "").strip()
69
+
70
+ @staticmethod
71
+ def verify_sha512(path: Path, expected: str) -> bool:
72
+ expected = str(expected or "").strip().lower()
73
+ if not expected:
74
+ return True
75
+ digest = hashlib.sha512()
76
+ with path.open("rb") as stream:
77
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
78
+ digest.update(chunk)
79
+ return digest.hexdigest().lower() == expected
80
+
81
+ def install_from_manifest(self, manifest: dict[str, Any]) -> str | None:
82
+ url = str(manifest.get("url") or "").strip()
83
+ version = str(manifest.get("version") or "").strip()
84
+ checksum = str(manifest.get("sha512") or "").strip()
85
+ if not url:
86
+ self._print("AGY install failed: manifest did not include a download URL.")
87
+ return None
88
+ install_dir = self.ports.user_bin_dir()
89
+ install_dir.mkdir(parents=True, exist_ok=True)
90
+ target = install_dir / ("agy.exe" if os.name == "nt" else "agy")
91
+ suffix = ".exe" if os.name == "nt" else ".tar.gz"
92
+ with tempfile.TemporaryDirectory(prefix="ciel-runtime-agy-") as temporary_dir:
93
+ download = Path(temporary_dir) / f"agy{suffix}"
94
+ self._print(f"Downloading AGY {version or 'latest'} from Google official manifest...")
95
+ self.download_file(url, download)
96
+ if not self.verify_sha512(download, checksum):
97
+ self._print("AGY install failed: sha512 verification did not match manifest.")
98
+ return None
99
+ if os.name == "nt":
100
+ shutil.copy2(download, target)
101
+ elif not self._extract_executable(download, target):
102
+ return None
103
+ try:
104
+ subprocess.run([str(target), "install"], input="y\n", text=True, env=self.ports.forced_environment(), timeout=120, check=False)
105
+ except Exception as exc:
106
+ self._print(f"AGY post-install setup skipped ({type(exc).__name__}); continuing.")
107
+ self._print(f"AGY installed: {target}")
108
+ return str(target)
109
+
110
+ def install_if_missing(self) -> str | None:
111
+ executable = self.ports.find_executable("agy")
112
+ if executable:
113
+ return executable
114
+ if os.environ.get("CIEL_RUNTIME_SKIP_AGY_INSTALL") == "1":
115
+ return None
116
+ manifest = self.latest_manifest()
117
+ if not manifest:
118
+ self._print("AGY executable was not found, and the official AGY manifest could not be read.")
119
+ return None
120
+ return self.install_from_manifest(manifest)
121
+
122
+ def update_check(self, executable: str, enabled: bool = True) -> str:
123
+ if not enabled or os.environ.get("CIEL_RUNTIME_SKIP_AGY_UPDATE") == "1":
124
+ return executable
125
+ self._print("Checking AGY update before launch...")
126
+ current = self.current_version(executable)
127
+ if current:
128
+ self._print(f"Current AGY version: {current}")
129
+ manifest = self.latest_manifest()
130
+ latest = str((manifest or {}).get("version") or "").strip()
131
+ if current and latest and not self.ports.version_newer(latest, current):
132
+ self._print(f"AGY is up to date ({current}).")
133
+ return executable
134
+ self._print(f"AGY update available: {current or 'unknown'} -> {latest}; upgrading automatically." if latest else "AGY update version could not be confirmed; running native updater.")
135
+ return_code, output = self.ports.run_upgrade([executable, "update"], timeout=300)
136
+ if output:
137
+ self._print(output)
138
+ if return_code != 0:
139
+ if manifest and latest and (not current or self.ports.version_newer(latest, current)):
140
+ return self.install_from_manifest(manifest) or executable
141
+ self._print(f"AGY update failed ({return_code}); continuing with current version.")
142
+ return executable
143
+ updated = self.ports.find_executable("agy") or executable
144
+ new_version = self.current_version(updated)
145
+ if new_version:
146
+ self._print(f"AGY version after update: {new_version}")
147
+ return updated
148
+
149
+ def _extract_executable(self, archive_path: Path, target: Path) -> bool:
150
+ with tarfile.open(archive_path, "r:gz") as archive:
151
+ member = next((item for item in archive.getmembers() if Path(item.name).name == "agy" and item.isfile()), None)
152
+ member = member or next((item for item in archive.getmembers() if item.isfile()), None)
153
+ if member is None:
154
+ self._print("AGY install failed: archive did not contain an executable file.")
155
+ return False
156
+ extracted = archive.extractfile(member)
157
+ if extracted is None:
158
+ self._print("AGY install failed: could not extract executable from archive.")
159
+ return False
160
+ with target.open("wb") as output:
161
+ shutil.copyfileobj(extracted, output)
162
+ target.chmod(target.stat().st_mode | 0o755)
163
+ return True
164
+
165
+ def _print(self, message: str) -> None:
166
+ self.ports.output(message, flush=True)
167
+
168
+
169
+ __all__ = ["AgyInstaller", "AgyInstallerPorts"]
@@ -0,0 +1,182 @@
1
+ """Restore missing managed MCP servers into AGY's native JSON configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from collections.abc import Callable, Mapping
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .mcp_inventory import McpInventoryService
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class AgyMcpRestorePorts:
18
+ discover_managed: Callable[[Path], dict[str, dict[str, Any]]]
19
+ log: Callable[[str, str], None]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class AgyMcpRestoreService:
24
+ ports: AgyMcpRestorePorts
25
+
26
+ def restore(
27
+ self,
28
+ env: Mapping[str, str] | None = None,
29
+ cwd: Path | None = None,
30
+ ) -> list[str]:
31
+ environment = os.environ if env is None else env
32
+ working_directory = (cwd or Path.cwd()).resolve()
33
+ home = self._home(environment)
34
+ global_path = home / ".gemini" / "antigravity-cli" / "mcp_config.json"
35
+ workspace_path = working_directory / ".agents" / "mcp_config.json"
36
+ try:
37
+ before = self._signature(global_path)
38
+ global_data = self._load(global_path)
39
+ workspace_data = self._load(workspace_path)
40
+ except (OSError, UnicodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
41
+ self.ports.log(
42
+ "WARN",
43
+ "agy_mcp_restore_config_read_failed "
44
+ f"error={type(exc).__name__}: {exc}",
45
+ )
46
+ return []
47
+
48
+ global_servers = self._servers(global_data)
49
+ reserved = set(self._servers(workspace_data))
50
+ projected: dict[str, dict[str, Any]] = {}
51
+ for name, server in self.ports.discover_managed(working_directory).items():
52
+ normalized = self._project_server(server)
53
+ if normalized is None:
54
+ self.ports.log(
55
+ "WARN", f"agy_mcp_restore_skipped_unsupported server={name}"
56
+ )
57
+ continue
58
+ projected[name] = normalized
59
+ merged = McpInventoryService.merge(
60
+ global_servers, projected, reserved=reserved
61
+ )
62
+ if not merged.added:
63
+ return []
64
+ restored = dict(global_data)
65
+ restored["mcpServers"] = merged.servers
66
+ try:
67
+ self._atomic_replace(global_path, restored, before)
68
+ except (OSError, UnicodeError, TypeError, ValueError, RuntimeError) as exc:
69
+ self.ports.log(
70
+ "WARN",
71
+ "agy_mcp_restore_write_failed "
72
+ f"path={global_path} error={type(exc).__name__}: {exc}",
73
+ )
74
+ return []
75
+ names = list(merged.added)
76
+ self.ports.log(
77
+ "INFO",
78
+ "agy_mcp_config_restored "
79
+ f"path={global_path} servers={','.join(sorted(names))}",
80
+ )
81
+ return names
82
+
83
+ @staticmethod
84
+ def _home(env: Mapping[str, str]) -> Path:
85
+ configured = str(env.get("HOME") or env.get("USERPROFILE") or "").strip()
86
+ return Path(configured).expanduser() if configured else Path.home()
87
+
88
+ @staticmethod
89
+ def _signature(path: Path) -> tuple[int, int, int] | None:
90
+ try:
91
+ stat = path.stat()
92
+ except FileNotFoundError:
93
+ return None
94
+ return stat.st_mtime_ns, stat.st_size, getattr(stat, "st_ino", 0)
95
+
96
+ @staticmethod
97
+ def _load(path: Path) -> dict[str, Any]:
98
+ if not path.is_file():
99
+ return {}
100
+ value = json.loads(path.read_text(encoding="utf-8"))
101
+ if not isinstance(value, dict):
102
+ raise ValueError(f"AGY MCP config must be a JSON object: {path}")
103
+ servers = value.get("mcpServers", {})
104
+ if not isinstance(servers, dict):
105
+ raise ValueError(f"AGY mcpServers must be a JSON object: {path}")
106
+ return value
107
+
108
+ @staticmethod
109
+ def _servers(data: dict[str, Any]) -> dict[str, dict[str, Any]]:
110
+ value = data.get("mcpServers", {})
111
+ return {
112
+ str(name): dict(server)
113
+ for name, server in value.items()
114
+ if str(name).strip() and isinstance(server, dict)
115
+ }
116
+
117
+ @staticmethod
118
+ def _project_server(server: dict[str, Any]) -> dict[str, Any] | None:
119
+ command = str(server.get("command") or "").strip()
120
+ url = str(
121
+ server.get("serverUrl")
122
+ or server.get("serverURL")
123
+ or server.get("url")
124
+ or server.get("endpoint")
125
+ or ""
126
+ ).strip()
127
+ args = server.get("args")
128
+ arguments = [str(item) for item in args] if isinstance(args, list) else []
129
+ if "mcp-proxy" in arguments and "--server-config" in arguments:
130
+ return None
131
+ if command:
132
+ projected: dict[str, Any] = {"command": command}
133
+ elif url.startswith(("http://", "https://")):
134
+ projected = {"serverUrl": url}
135
+ else:
136
+ return None
137
+ aliases = {
138
+ "http_headers": "headers",
139
+ "env_http_headers": "headers",
140
+ "type": "transport",
141
+ }
142
+ supported = (
143
+ "args", "env", "headers", "transport", "authProviderType",
144
+ )
145
+ normalized = dict(server)
146
+ for source, target in aliases.items():
147
+ if target not in normalized and source in normalized:
148
+ normalized[target] = normalized[source]
149
+ for key in supported:
150
+ value = normalized.get(key)
151
+ if isinstance(value, (str, bool, int, float, list, dict)):
152
+ projected[key] = value
153
+ return projected
154
+
155
+ @classmethod
156
+ def _atomic_replace(
157
+ cls,
158
+ path: Path,
159
+ value: dict[str, Any],
160
+ expected: tuple[int, int, int] | None,
161
+ ) -> None:
162
+ path.parent.mkdir(parents=True, exist_ok=True)
163
+ if cls._signature(path) != expected:
164
+ raise RuntimeError("AGY config changed while MCP restoration was running")
165
+ temporary = path.with_name(f"{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
166
+ try:
167
+ temporary.write_text(
168
+ json.dumps(value, indent=2, ensure_ascii=False) + "\n",
169
+ encoding="utf-8",
170
+ )
171
+ os.chmod(temporary, 0o600)
172
+ if cls._signature(path) != expected:
173
+ raise RuntimeError("AGY config changed while MCP restoration was running")
174
+ temporary.replace(path)
175
+ finally:
176
+ try:
177
+ temporary.unlink()
178
+ except FileNotFoundError:
179
+ pass
180
+
181
+
182
+ __all__ = ["AgyMcpRestorePorts", "AgyMcpRestoreService"]