@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

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 (136) hide show
  1. package/ciel_runtime.py +2555 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -0,0 +1,309 @@
1
+ """Runtime installation, update, diagnostics, and upgrade bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Mapping
8
+
9
+ from .agy_installer import AgyInstaller
10
+ from .install_diagnostics import InstallDiagnosticsService
11
+ from .package_lifecycle import NpmPackageLifecycle, SelfUpdateLifecycle
12
+ from .runtime_restart import RuntimeRestartService
13
+ from .runtime_upgrade import RuntimeUpgradeService
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class RuntimePackagePorts:
18
+ lifecycle: Callable[[], NpmPackageLifecycle]
19
+ environment: Mapping[str, str]
20
+ claude_version: Callable[[str], str]
21
+ codex_version: Callable[[str], str]
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class RuntimeLifecyclePorts:
26
+ diagnostics: Callable[[], InstallDiagnosticsService]
27
+ restart: Callable[[], RuntimeRestartService]
28
+ self_update: Callable[[], SelfUpdateLifecycle]
29
+ upgrade: Callable[[], RuntimeUpgradeService]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class RuntimeAgyPorts:
34
+ installer: Callable[[], AgyInstaller]
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class RuntimeUpgradeCommandPorts:
39
+ ciel_runtime: Callable[[], int]
40
+ claude: Callable[[], int]
41
+ codex: Callable[[], int]
42
+ agy: Callable[[], int]
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class RuntimeMaintenanceContext:
47
+ packages: RuntimePackagePorts
48
+ lifecycle: RuntimeLifecyclePorts
49
+ agy: RuntimeAgyPorts
50
+ upgrade_commands: RuntimeUpgradeCommandPorts
51
+
52
+ def install_runtime_package_if_missing(
53
+ self,
54
+ *,
55
+ executable_name: str,
56
+ label: str,
57
+ package_spec: str,
58
+ skip_env: str,
59
+ ) -> str | None:
60
+ return self.packages.lifecycle().install_if_missing(
61
+ executable_name=executable_name,
62
+ label=label,
63
+ package_spec=package_spec,
64
+ skip_env=skip_env,
65
+ )
66
+
67
+ def run_runtime_npm_update_check(
68
+ self,
69
+ executable: str,
70
+ *,
71
+ executable_name: str,
72
+ label: str,
73
+ package_spec: str,
74
+ skip_env: str,
75
+ current_version: Callable[[str], str],
76
+ enabled: bool = True,
77
+ ) -> str:
78
+ return self.packages.lifecycle().update_check(
79
+ executable,
80
+ executable_name=executable_name,
81
+ label=label,
82
+ package_spec=package_spec,
83
+ skip_env=skip_env,
84
+ current_version=current_version,
85
+ enabled=enabled,
86
+ )
87
+
88
+ def run_claude_update_check(self, executable: str, enabled: bool = True) -> str:
89
+ package = self.packages.environment.get(
90
+ "CIEL_RUNTIME_CLAUDE_CODE_PACKAGE", "@anthropic-ai/claude-code@latest"
91
+ )
92
+ return self.run_runtime_npm_update_check(
93
+ executable,
94
+ executable_name="claude",
95
+ label="Claude Code",
96
+ package_spec=package,
97
+ skip_env="CIEL_RUNTIME_SKIP_CLAUDE_UPDATE",
98
+ current_version=self.packages.claude_version,
99
+ enabled=enabled,
100
+ )
101
+
102
+ def run_codex_update_check(self, executable: str, enabled: bool = True) -> str:
103
+ package = self.packages.environment.get(
104
+ "CIEL_RUNTIME_CODEX_PACKAGE", "@openai/codex@latest"
105
+ )
106
+ return self.run_runtime_npm_update_check(
107
+ executable,
108
+ executable_name="codex",
109
+ label="Codex",
110
+ package_spec=package,
111
+ skip_env="CIEL_RUNTIME_SKIP_CODEX_UPDATE",
112
+ current_version=self.packages.codex_version,
113
+ enabled=enabled,
114
+ )
115
+
116
+ def install_claude_code_if_missing(self) -> str | None:
117
+ package = self.packages.environment.get(
118
+ "CIEL_RUNTIME_CLAUDE_CODE_PACKAGE", "@anthropic-ai/claude-code@latest"
119
+ )
120
+ return self.install_runtime_package_if_missing(
121
+ executable_name="claude",
122
+ label="Claude Code",
123
+ package_spec=package,
124
+ skip_env="CIEL_RUNTIME_SKIP_CLAUDE_INSTALL",
125
+ )
126
+
127
+ def install_codex_if_missing(self) -> str | None:
128
+ package = self.packages.environment.get(
129
+ "CIEL_RUNTIME_CODEX_PACKAGE", "@openai/codex@latest"
130
+ )
131
+ return self.install_runtime_package_if_missing(
132
+ executable_name="codex",
133
+ label="Codex",
134
+ package_spec=package,
135
+ skip_env="CIEL_RUNTIME_SKIP_CODEX_INSTALL",
136
+ )
137
+
138
+ def launcher_candidate_dirs(self) -> list[Path]:
139
+ return self.lifecycle.diagnostics().candidate_dirs()
140
+
141
+ def launcher_candidates(self) -> list[Path]:
142
+ return self.lifecycle.diagnostics().candidates()
143
+
144
+ def launcher_version(self, path: Path, timeout: float = 5.0) -> str:
145
+ return self.lifecycle.diagnostics().launcher_version(path, timeout)
146
+
147
+ def install_diagnostics(self) -> list[dict[str, str]]:
148
+ return self.lifecycle.diagnostics().diagnostics()
149
+
150
+ def warn_if_multiple_installs(self) -> None:
151
+ self.lifecycle.diagnostics().warn_if_multiple()
152
+
153
+ def restart_user_args(self) -> list[str]:
154
+ return self.lifecycle.restart().user_args()
155
+
156
+ def restart_after_update(self, npm: str, package_root: Path | None = None) -> None:
157
+ self.lifecycle.restart().restart(npm, package_root)
158
+
159
+ def run_self_update_check(self, enabled: bool = True) -> bool:
160
+ return self.lifecycle.self_update().run(enabled)
161
+
162
+ def quiet_upgrade_ciel_runtime(self) -> int:
163
+ return self.lifecycle.upgrade().ciel_runtime()
164
+
165
+ def quiet_upgrade_claude_code(self) -> int:
166
+ return self.lifecycle.upgrade().claude()
167
+
168
+ def quiet_upgrade_codex(self) -> int:
169
+ return self.lifecycle.upgrade().codex()
170
+
171
+ def quiet_upgrade_agy(self) -> int:
172
+ return self.lifecycle.upgrade().agy()
173
+
174
+ def agy_manifest_name(self) -> str:
175
+ return self.agy.installer().manifest_name()
176
+
177
+ def agy_manifest_url(self) -> str:
178
+ return self.agy.installer().manifest_url()
179
+
180
+ def agy_latest_manifest(self, timeout: float = 15.0) -> dict[str, Any] | None:
181
+ return self.agy.installer().latest_manifest(timeout)
182
+
183
+ def install_agy_from_manifest(self, manifest: dict[str, Any]) -> str | None:
184
+ return self.agy.installer().install_from_manifest(manifest)
185
+
186
+ def install_agy_if_missing(self) -> str | None:
187
+ return self.agy.installer().install_if_missing()
188
+
189
+ def run_agy_update_check(self, executable: str, enabled: bool = True) -> str:
190
+ return self.agy.installer().update_check(executable, enabled)
191
+
192
+ def run_quiet_upgrade_and_exit(self) -> int:
193
+ results = (
194
+ self.upgrade_commands.ciel_runtime(),
195
+ self.upgrade_commands.claude(),
196
+ self.upgrade_commands.codex(),
197
+ self.upgrade_commands.agy(),
198
+ )
199
+ return 0 if all(result == 0 for result in results) else 1
200
+
201
+
202
+ @dataclass(frozen=True, slots=True)
203
+ class RuntimeMaintenanceCompatibilityApi:
204
+ """Late-bound compatibility API for the thin runtime facade."""
205
+
206
+ context: Callable[[], RuntimeMaintenanceContext]
207
+
208
+ def install_runtime_package_if_missing(self, **options: str) -> str | None:
209
+ return self.context().install_runtime_package_if_missing(**options)
210
+
211
+ def run_runtime_npm_update_check(
212
+ self,
213
+ executable: str,
214
+ *,
215
+ executable_name: str,
216
+ label: str,
217
+ package_spec: str,
218
+ skip_env: str,
219
+ current_version: Callable[[str], str],
220
+ enabled: bool = True,
221
+ ) -> str:
222
+ return self.context().run_runtime_npm_update_check(
223
+ executable,
224
+ executable_name=executable_name,
225
+ label=label,
226
+ package_spec=package_spec,
227
+ skip_env=skip_env,
228
+ current_version=current_version,
229
+ enabled=enabled,
230
+ )
231
+
232
+ def run_claude_update_check(self, executable: str, enabled: bool = True) -> str:
233
+ return self.context().run_claude_update_check(executable, enabled)
234
+
235
+ def run_codex_update_check(self, executable: str, enabled: bool = True) -> str:
236
+ return self.context().run_codex_update_check(executable, enabled)
237
+
238
+ def install_claude_code_if_missing(self) -> str | None:
239
+ return self.context().install_claude_code_if_missing()
240
+
241
+ def install_codex_if_missing(self) -> str | None:
242
+ return self.context().install_codex_if_missing()
243
+
244
+ def launcher_candidate_dirs(self) -> list[Path]:
245
+ return self.context().launcher_candidate_dirs()
246
+
247
+ def launcher_candidates(self) -> list[Path]:
248
+ return self.context().launcher_candidates()
249
+
250
+ def launcher_version(self, path: Path, timeout: float = 5.0) -> str:
251
+ return self.context().launcher_version(path, timeout)
252
+
253
+ def install_diagnostics(self) -> list[dict[str, str]]:
254
+ return self.context().install_diagnostics()
255
+
256
+ def warn_if_multiple_installs(self) -> None:
257
+ self.context().warn_if_multiple_installs()
258
+
259
+ def restart_user_args(self) -> list[str]:
260
+ return self.context().restart_user_args()
261
+
262
+ def restart_after_update(self, npm: str, package_root: Path | None = None) -> None:
263
+ self.context().restart_after_update(npm, package_root)
264
+
265
+ def run_self_update_check(self, enabled: bool = True) -> bool:
266
+ return self.context().run_self_update_check(enabled)
267
+
268
+ def quiet_upgrade_ciel_runtime(self) -> int:
269
+ return self.context().quiet_upgrade_ciel_runtime()
270
+
271
+ def quiet_upgrade_claude_code(self) -> int:
272
+ return self.context().quiet_upgrade_claude_code()
273
+
274
+ def quiet_upgrade_codex(self) -> int:
275
+ return self.context().quiet_upgrade_codex()
276
+
277
+ def quiet_upgrade_agy(self) -> int:
278
+ return self.context().quiet_upgrade_agy()
279
+
280
+ def agy_manifest_name(self) -> str:
281
+ return self.context().agy_manifest_name()
282
+
283
+ def agy_manifest_url(self) -> str:
284
+ return self.context().agy_manifest_url()
285
+
286
+ def agy_latest_manifest(self, timeout: float = 15.0) -> dict[str, Any] | None:
287
+ return self.context().agy_latest_manifest(timeout)
288
+
289
+ def install_agy_from_manifest(self, manifest: dict[str, Any]) -> str | None:
290
+ return self.context().install_agy_from_manifest(manifest)
291
+
292
+ def install_agy_if_missing(self) -> str | None:
293
+ return self.context().install_agy_if_missing()
294
+
295
+ def run_agy_update_check(self, executable: str, enabled: bool = True) -> str:
296
+ return self.context().run_agy_update_check(executable, enabled)
297
+
298
+ def run_quiet_upgrade_and_exit(self) -> int:
299
+ return self.context().run_quiet_upgrade_and_exit()
300
+
301
+
302
+ __all__ = [
303
+ "RuntimeAgyPorts",
304
+ "RuntimeLifecyclePorts",
305
+ "RuntimeMaintenanceCompatibilityApi",
306
+ "RuntimeMaintenanceContext",
307
+ "RuntimePackagePorts",
308
+ "RuntimeUpgradeCommandPorts",
309
+ ]
@@ -0,0 +1,265 @@
1
+ """Typed service composition for runtime installation and maintenance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Mapping
8
+
9
+ from .agy_installer import AgyInstaller, AgyInstallerPorts
10
+ from .install_diagnostics import (
11
+ InstallDiagnosticsPorts,
12
+ InstallDiagnosticsService,
13
+ InstallDiagnosticsSettings,
14
+ )
15
+ from .package_lifecycle import (
16
+ NpmPackageLifecycle,
17
+ NpmPackageLifecyclePorts,
18
+ SelfUpdateLifecycle,
19
+ SelfUpdatePorts,
20
+ )
21
+ from .runtime_restart import RuntimeRestartPorts, RuntimeRestartService, RuntimeRestartSettings
22
+ from .runtime_upgrade import (
23
+ RuntimeUpgradeNpmPorts,
24
+ RuntimeUpgradeService,
25
+ RuntimeUpgradeSettings,
26
+ RuntimeUpgradeToolPorts,
27
+ )
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class MaintenanceNpmPorts:
32
+ find_executable: Callable[[str], str | None]
33
+ install_command: Callable[..., list[str]]
34
+ run_upgrade: Callable[..., tuple[int, str]]
35
+ add_prefix_bin: Callable[[Path], None]
36
+ latest_version: Callable[..., str | None]
37
+ version_newer: Callable[[str, str], bool]
38
+ output: Callable[..., None]
39
+ current_prefix: Callable[[], Path | None]
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class MaintenancePackagePorts:
44
+ entrypoint: Path
45
+ environment: Mapping[str, str]
46
+ package_root: Callable[[Path], Path | None]
47
+ prefix_from_root: Callable[[Path], Path | None]
48
+ running_from_package: Callable[[Path, Mapping[str, str]], bool]
49
+ global_package_root: Callable[..., Path | None]
50
+ global_install_command: Callable[..., list[str]]
51
+ current_root: Callable[[], Path | None]
52
+ running_from_npm: Callable[[], bool]
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class MaintenanceDiagnosticPorts:
57
+ home: Path
58
+ platform_name: str
59
+ extra_dirs: Callable[[], list[Path]]
60
+ parse_version: Callable[[str], tuple[int, ...]]
61
+ diagnostics: Callable[[], list[dict[str, str]]]
62
+ stdin_isatty: Callable[[], bool]
63
+ stdout_isatty: Callable[[], bool]
64
+ write_error: Callable[[str], None]
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class MaintenanceRestartPorts:
69
+ argv: list[str]
70
+ executable: str
71
+ environment: dict[str, str]
72
+ execv: Callable[..., Any]
73
+ call: Callable[..., int]
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class MaintenanceUpdatePorts:
78
+ version: str
79
+ forced_environment: Callable[[], dict[str, str]]
80
+ restart_after_update: Callable[..., None]
81
+ claude_version: Callable[[str], str]
82
+ codex_version: Callable[[str], str]
83
+ install_claude: Callable[[], str | None]
84
+ install_codex: Callable[[], str | None]
85
+ install_agy: Callable[[], str | None]
86
+ update_agy: Callable[[str, bool], str]
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class MaintenanceAgyPorts:
91
+ manifest_base_url: str
92
+ user_bin_dir: Callable[[], Path]
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class RuntimeMaintenanceServices:
97
+ npm: MaintenanceNpmPorts
98
+ package: MaintenancePackagePorts
99
+ diagnostics: MaintenanceDiagnosticPorts
100
+ restart: MaintenanceRestartPorts
101
+ update: MaintenanceUpdatePorts
102
+ agy: MaintenanceAgyPorts
103
+
104
+ def current_package_root(self) -> Path | None:
105
+ return self.package.package_root(self.package.entrypoint)
106
+
107
+ def current_prefix(self) -> Path | None:
108
+ root = self.current_package_root()
109
+ return self.package.prefix_from_root(root) if root else None
110
+
111
+ def running_from_npm_package(self) -> bool:
112
+ return self.package.running_from_package(
113
+ self.package.entrypoint, self.package.environment
114
+ )
115
+
116
+ def npm_lifecycle(self) -> NpmPackageLifecycle:
117
+ return NpmPackageLifecycle(
118
+ NpmPackageLifecyclePorts(
119
+ self.npm.find_executable,
120
+ self.npm.current_prefix,
121
+ self.npm.install_command,
122
+ self.npm.run_upgrade,
123
+ self.npm.add_prefix_bin,
124
+ self.npm.latest_version,
125
+ self.npm.version_newer,
126
+ self.npm.output,
127
+ )
128
+ )
129
+
130
+ def install_diagnostics(self) -> InstallDiagnosticsService:
131
+ return InstallDiagnosticsService(
132
+ settings=InstallDiagnosticsSettings(
133
+ self.diagnostics.home,
134
+ self.package.environment,
135
+ self.diagnostics.platform_name == "nt",
136
+ ),
137
+ ports=InstallDiagnosticsPorts(
138
+ extra_dirs=self.diagnostics.extra_dirs,
139
+ package_root=self.package.package_root,
140
+ current_root=self.current_package_root,
141
+ parse_version=self.diagnostics.parse_version,
142
+ diagnostics=self.diagnostics.diagnostics,
143
+ stdin_isatty=self.diagnostics.stdin_isatty,
144
+ stdout_isatty=self.diagnostics.stdout_isatty,
145
+ write_error=self.diagnostics.write_error,
146
+ ),
147
+ )
148
+
149
+ def restart_service(self) -> RuntimeRestartService:
150
+ return RuntimeRestartService(
151
+ settings=RuntimeRestartSettings(
152
+ self.restart.argv,
153
+ self.restart.executable,
154
+ self.restart.environment,
155
+ platform_name=self.diagnostics.platform_name,
156
+ ),
157
+ ports=RuntimeRestartPorts(
158
+ current_package_root=self.current_package_root,
159
+ global_package_root=self.package.global_package_root,
160
+ find_executable=self.npm.find_executable,
161
+ execv=self.restart.execv,
162
+ call=self.restart.call,
163
+ ),
164
+ )
165
+
166
+ def self_update(self) -> SelfUpdateLifecycle:
167
+ return SelfUpdateLifecycle(
168
+ self.update.version,
169
+ SelfUpdatePorts(
170
+ self.package.running_from_npm,
171
+ self.npm.find_executable,
172
+ self.npm.latest_version,
173
+ self.npm.version_newer,
174
+ self.package.current_root,
175
+ self.package.prefix_from_root,
176
+ self.package.global_install_command,
177
+ self.update.forced_environment,
178
+ self.update.restart_after_update,
179
+ self.npm.output,
180
+ ),
181
+ )
182
+
183
+ def upgrade(self) -> RuntimeUpgradeService:
184
+ return RuntimeUpgradeService(
185
+ settings=RuntimeUpgradeSettings(
186
+ self.update.version, self.package.environment
187
+ ),
188
+ npm=RuntimeUpgradeNpmPorts(
189
+ find_executable=self.npm.find_executable,
190
+ latest_version=self.npm.latest_version,
191
+ version_newer=self.npm.version_newer,
192
+ current_package_root=self.package.current_root,
193
+ package_prefix=self.package.prefix_from_root,
194
+ current_prefix=self.npm.current_prefix,
195
+ global_install_command=self.package.global_install_command,
196
+ runtime_install_command=self.npm.install_command,
197
+ run_command=self.npm.run_upgrade,
198
+ ),
199
+ tools=RuntimeUpgradeToolPorts(
200
+ claude_version=self.update.claude_version,
201
+ codex_version=self.update.codex_version,
202
+ install_claude=self.update.install_claude,
203
+ install_codex=self.update.install_codex,
204
+ install_agy=self.update.install_agy,
205
+ update_agy=self.update.update_agy,
206
+ ),
207
+ output=lambda message: self.npm.output(message, flush=True),
208
+ )
209
+
210
+ def agy_installer(self) -> AgyInstaller:
211
+ return AgyInstaller(
212
+ self.agy.manifest_base_url,
213
+ AgyInstallerPorts(
214
+ self.agy.user_bin_dir,
215
+ self.update.forced_environment,
216
+ self.npm.find_executable,
217
+ self.npm.version_newer,
218
+ self.npm.run_upgrade,
219
+ self.npm.output,
220
+ ),
221
+ )
222
+
223
+
224
+ @dataclass(frozen=True, slots=True)
225
+ class RuntimeMaintenanceServicesCompatibilityApi:
226
+ services: Callable[[], RuntimeMaintenanceServices]
227
+
228
+ def npm_lifecycle(self) -> NpmPackageLifecycle:
229
+ return self.services().npm_lifecycle()
230
+
231
+ def current_prefix(self) -> Path | None:
232
+ return self.services().current_prefix()
233
+
234
+ def running_from_npm_package(self) -> bool:
235
+ return self.services().running_from_npm_package()
236
+
237
+ def current_package_root(self) -> Path | None:
238
+ return self.services().current_package_root()
239
+
240
+ def install_diagnostics(self) -> InstallDiagnosticsService:
241
+ return self.services().install_diagnostics()
242
+
243
+ def restart_service(self) -> RuntimeRestartService:
244
+ return self.services().restart_service()
245
+
246
+ def self_update(self) -> SelfUpdateLifecycle:
247
+ return self.services().self_update()
248
+
249
+ def upgrade(self) -> RuntimeUpgradeService:
250
+ return self.services().upgrade()
251
+
252
+ def agy_installer(self) -> AgyInstaller:
253
+ return self.services().agy_installer()
254
+
255
+
256
+ __all__ = [
257
+ "MaintenanceAgyPorts",
258
+ "MaintenanceDiagnosticPorts",
259
+ "MaintenanceNpmPorts",
260
+ "MaintenancePackagePorts",
261
+ "MaintenanceRestartPorts",
262
+ "MaintenanceUpdatePorts",
263
+ "RuntimeMaintenanceServices",
264
+ "RuntimeMaintenanceServicesCompatibilityApi",
265
+ ]
@@ -9,6 +9,7 @@ from pathlib import Path, PureWindowsPath
9
9
  from typing import Any
10
10
 
11
11
  from .web_endpoints import apply_startup_web_options, load_saved_web_backend
12
+ from .workspace_router_selection import select_workspace_router_port, workspace_identity
12
13
 
13
14
 
14
15
  # Web startup flags affect the process-wide router endpoint constants below.
@@ -96,52 +97,71 @@ def default_router_port(saved_port: int = 0) -> int:
96
97
 
97
98
  CONFIG_DIR = Path(os.environ.get("CIEL_RUNTIME_CONFIG_DIR") or platform_config_dir("ciel-runtime"))
98
99
  CONFIG_PATH = CONFIG_DIR / "config.json"
99
- LOG_PATH = CONFIG_DIR / "router.log"
100
- LOG_LEVEL_PATH = CONFIG_DIR / "log-level"
101
- REQUEST_DUMP_PATH = CONFIG_DIR / "requests.jsonl"
102
- RESPONSE_DUMP_PATH = CONFIG_DIR / "responses.jsonl"
103
- USAGE_EVENTS_PATH = CONFIG_DIR / "usage-events.jsonl"
104
- SSE_TRACE_PATH = CONFIG_DIR / "router-sse-trace.jsonl"
105
- SSE_LAST_PATH = CONFIG_DIR / "router-last-sse.json"
106
- TOOL_CALL_LOG_PATH = CONFIG_DIR / "tool-calls.jsonl"
107
- RATE_LIMIT_STATE_PATH = CONFIG_DIR / "rate-limit-state.json"
108
- ROUTER_ACTIVITY_PATH = CONFIG_DIR / "router-activity.json"
109
- CONTEXT_COMPACT_ACTIVITY_PATH = CONFIG_DIR / "context-compact-activity.json"
110
- CONTEXT_USAGE_PATH = CONFIG_DIR / "context-usage.json"
111
- OLLAMA_MODEL_CATALOG_PATH = CONFIG_DIR / "ollama-model-catalog.json"
112
- CHAT_MESSAGES_PATH = CONFIG_DIR / "chat-messages.jsonl"
113
- CHAT_FILES_DIR = CONFIG_DIR / "chat-files"
114
- MENU_KEY_DEBUG_PATH = CONFIG_DIR / "ca-key-debug.log"
115
- PLAN_ARTIFACTS_DIR = CONFIG_DIR / "plan-artifacts"
116
- PID_PATH = CONFIG_DIR / "router.pid"
117
- ROUTER_EXTERNAL_TOKEN_PATH = CONFIG_DIR / "router-external-token"
118
- ROUTER_CLIENTS_DIR = CONFIG_DIR / "router-clients"
119
- MODEL_LIST_CACHE_PATH = CONFIG_DIR / "model-list-cache.json"
120
- MODEL_REGISTRY_PATH = CONFIG_DIR / "model-registry.json"
121
- LAUNCH_STATE_PATH = CONFIG_DIR / "launch-state.json"
122
- WEB_TOOLS_MCP_CONFIG = CONFIG_DIR / "web-tools-mcp.json"
123
- DUCKDUCKGO_MCP_CONFIG = CONFIG_DIR / "duckduckgo-mcp.json"
124
- ZAI_MCP_CONFIG = CONFIG_DIR / "zai-mcp.json"
125
- CHANNEL_MCP_CONFIG = CONFIG_DIR / "channel-mcp.json"
126
- NATIVE_MCP_CONFIG = CONFIG_DIR / "native-mcp.json"
127
- CODEX_MCP_CONFIG = CONFIG_DIR / "codex-mcp.json"
128
- CODEX_PROCESS_DIR = CONFIG_DIR / "codex-processes"
129
- CODEX_PROMPTS_DIR_NAME = "prompts"
130
- CHANNEL_MCP_CURSOR_PATH = CONFIG_DIR / "channel-mcp-cursor.json"
131
- CHANNEL_LLM_CURSOR_PATH = CONFIG_DIR / "channel-llm-cursor.json"
132
- CHANNEL_LLM_CLEAR_FLOOR_PATH = CONFIG_DIR / "channel-llm-clear-floor.json"
133
- CHANNEL_LLM_LAUNCH_GUARD_PATH = CONFIG_DIR / "channel-llm-launch-guard.json"
134
- CHANNEL_COMPACT_REQUEST_PATH = CONFIG_DIR / "channel-compact-request.json"
135
- CHANNEL_STDIN_WAKE_CLAIMS_PATH = CONFIG_DIR / "channel-stdin-wake-claims.json"
136
- CHANNEL_PROBE_CACHE_PATH = CONFIG_DIR / "channel-probe-cache.json"
137
- MCP_PROXY_CONFIG = CONFIG_DIR / "mcp-proxy.json"
138
100
  _SAVED_WEB_BACKEND = load_saved_web_backend(CONFIG_PATH)
139
101
  ROUTER_HOST = (
140
102
  os.environ.get("CIEL_RUNTIME_ROUTER_CLIENT_HOST", "").strip()
141
103
  or _SAVED_WEB_BACKEND.client_host
142
104
  )
143
- ROUTER_PORT = default_router_port(_SAVED_WEB_BACKEND.port)
105
+ _BASE_ROUTER_PORT = default_router_port(_SAVED_WEB_BACKEND.port)
106
+ ROUTER_PORT = select_workspace_router_port(_BASE_ROUTER_PORT, Path.cwd(), os.environ)
144
107
  ROUTER_BASE = f"http://{ROUTER_HOST}:{ROUTER_PORT}"
108
+ ROUTER_WORKSPACE = workspace_identity(
109
+ os.environ.get("CIEL_RUNTIME_LAUNCH_CWD") or Path.cwd()
110
+ )
111
+ _WORKSPACE_DIGEST = hashlib.sha256(ROUTER_WORKSPACE.encode("utf-8", errors="replace")).hexdigest()[:12]
112
+ _STATE_DIR_OVERRIDE = str(os.environ.get("CIEL_RUNTIME_STATE_DIR") or "").strip()
113
+ _TEST_STATE_ISOLATED = str(os.environ.get("CIEL_RUNTIME_TEST_ISOLATED") or "").strip().lower() in {
114
+ "1",
115
+ "true",
116
+ "yes",
117
+ "on",
118
+ }
119
+ ROUTER_INSTANCE_DIR = (
120
+ Path(_STATE_DIR_OVERRIDE)
121
+ if _STATE_DIR_OVERRIDE
122
+ else CONFIG_DIR
123
+ if _TEST_STATE_ISOLATED
124
+ else CONFIG_DIR / "router-instances" / f"{ROUTER_PORT}-{_WORKSPACE_DIGEST}"
125
+ )
126
+ LOG_PATH = ROUTER_INSTANCE_DIR / "router.log"
127
+ LOG_LEVEL_PATH = CONFIG_DIR / "log-level"
128
+ REQUEST_DUMP_PATH = ROUTER_INSTANCE_DIR / "requests.jsonl"
129
+ RESPONSE_DUMP_PATH = ROUTER_INSTANCE_DIR / "responses.jsonl"
130
+ USAGE_EVENTS_PATH = ROUTER_INSTANCE_DIR / "usage-events.jsonl"
131
+ SSE_TRACE_PATH = ROUTER_INSTANCE_DIR / "router-sse-trace.jsonl"
132
+ SSE_LAST_PATH = ROUTER_INSTANCE_DIR / "router-last-sse.json"
133
+ TOOL_CALL_LOG_PATH = ROUTER_INSTANCE_DIR / "tool-calls.jsonl"
134
+ RATE_LIMIT_STATE_PATH = ROUTER_INSTANCE_DIR / "rate-limit-state.json"
135
+ ROUTER_ACTIVITY_PATH = ROUTER_INSTANCE_DIR / "router-activity.json"
136
+ CONTEXT_COMPACT_ACTIVITY_PATH = ROUTER_INSTANCE_DIR / "context-compact-activity.json"
137
+ CONTEXT_USAGE_PATH = ROUTER_INSTANCE_DIR / "context-usage.json"
138
+ OLLAMA_MODEL_CATALOG_PATH = CONFIG_DIR / "ollama-model-catalog.json"
139
+ CHAT_MESSAGES_PATH = ROUTER_INSTANCE_DIR / "chat-messages.jsonl"
140
+ CHAT_FILES_DIR = ROUTER_INSTANCE_DIR / "chat-files"
141
+ MENU_KEY_DEBUG_PATH = ROUTER_INSTANCE_DIR / "ca-key-debug.log"
142
+ PLAN_ARTIFACTS_DIR = ROUTER_INSTANCE_DIR / "plan-artifacts"
143
+ PID_PATH = ROUTER_INSTANCE_DIR / "router.pid"
144
+ ROUTER_EXTERNAL_TOKEN_PATH = ROUTER_INSTANCE_DIR / "router-external-token"
145
+ ROUTER_CLIENTS_DIR = ROUTER_INSTANCE_DIR / "router-clients"
146
+ MODEL_LIST_CACHE_PATH = CONFIG_DIR / "model-list-cache.json"
147
+ MODEL_REGISTRY_PATH = CONFIG_DIR / "model-registry.json"
148
+ LAUNCH_STATE_PATH = CONFIG_DIR / "launch-state.json"
149
+ WEB_TOOLS_MCP_CONFIG = ROUTER_INSTANCE_DIR / "web-tools-mcp.json"
150
+ DUCKDUCKGO_MCP_CONFIG = ROUTER_INSTANCE_DIR / "duckduckgo-mcp.json"
151
+ ZAI_MCP_CONFIG = ROUTER_INSTANCE_DIR / "zai-mcp.json"
152
+ CHANNEL_MCP_CONFIG = ROUTER_INSTANCE_DIR / "channel-mcp.json"
153
+ NATIVE_MCP_CONFIG = ROUTER_INSTANCE_DIR / "native-mcp.json"
154
+ CODEX_MCP_CONFIG = ROUTER_INSTANCE_DIR / "codex-mcp.json"
155
+ CODEX_PROCESS_DIR = ROUTER_INSTANCE_DIR / "codex-processes"
156
+ CODEX_PROMPTS_DIR_NAME = "prompts"
157
+ CHANNEL_MCP_CURSOR_PATH = ROUTER_INSTANCE_DIR / "channel-mcp-cursor.json"
158
+ CHANNEL_LLM_CURSOR_PATH = ROUTER_INSTANCE_DIR / "channel-llm-cursor.json"
159
+ CHANNEL_LLM_CLEAR_FLOOR_PATH = ROUTER_INSTANCE_DIR / "channel-llm-clear-floor.json"
160
+ CHANNEL_LLM_LAUNCH_GUARD_PATH = ROUTER_INSTANCE_DIR / "channel-llm-launch-guard.json"
161
+ CHANNEL_COMPACT_REQUEST_PATH = ROUTER_INSTANCE_DIR / "channel-compact-request.json"
162
+ CHANNEL_STDIN_WAKE_CLAIMS_PATH = ROUTER_INSTANCE_DIR / "channel-stdin-wake-claims.json"
163
+ CHANNEL_PROBE_CACHE_PATH = ROUTER_INSTANCE_DIR / "channel-probe-cache.json"
164
+ MCP_PROXY_CONFIG = ROUTER_INSTANCE_DIR / "mcp-proxy.json"
145
165
  CLAUDE_GATEWAY_CACHE = HOME / ".claude" / "cache" / "gateway-models.json"
146
166
  CLAUDE_SETTINGS_PATH = HOME / ".claude" / "settings.json"
147
167
  CLAUDE_COMMANDS_DIR = HOME / ".claude" / "commands"