@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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Router HTTP server startup and shutdown application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class RouterServerConfig:
|
|
12
|
+
config_dir: Path
|
|
13
|
+
pid_path: Path
|
|
14
|
+
port: int
|
|
15
|
+
client_base: str
|
|
16
|
+
log_level_path: Path
|
|
17
|
+
log_level_names: dict[Any, Any]
|
|
18
|
+
handler: Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class RouterServerStatePorts:
|
|
23
|
+
load_config: Callable[[], dict[str, Any]]
|
|
24
|
+
reset_api_key_cooldowns: Callable[[], None]
|
|
25
|
+
bind_host: Callable[[dict[str, Any]], str]
|
|
26
|
+
current_log_level: Callable[[], Any]
|
|
27
|
+
current_pid: Callable[[], int]
|
|
28
|
+
env_value: Callable[[str], str | None]
|
|
29
|
+
external_access_enabled: Callable[[dict[str, Any]], bool]
|
|
30
|
+
ensure_external_token: Callable[[], str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class RouterServerEffects:
|
|
35
|
+
chmod: Callable[[Path, int], None]
|
|
36
|
+
stderr: Any
|
|
37
|
+
server_factory: Callable[..., Any]
|
|
38
|
+
start_watchdog: Callable[[Any], None]
|
|
39
|
+
start_channels: Callable[[dict[str, Any]], Any]
|
|
40
|
+
stop_channels: Callable[[None], Any]
|
|
41
|
+
thread_factory: Callable[..., Any]
|
|
42
|
+
configure_web_endpoints: Callable[[str], list[str]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class RouterServerRuntime:
|
|
47
|
+
config: RouterServerConfig
|
|
48
|
+
state: RouterServerStatePorts
|
|
49
|
+
effects: RouterServerEffects
|
|
50
|
+
|
|
51
|
+
def run(self) -> None:
|
|
52
|
+
self.config.config_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
runtime_config = self.state.load_config()
|
|
54
|
+
self.state.reset_api_key_cooldowns()
|
|
55
|
+
bind_host = self.state.bind_host(runtime_config)
|
|
56
|
+
if self.state.external_access_enabled(runtime_config):
|
|
57
|
+
self.state.ensure_external_token()
|
|
58
|
+
self.config.pid_path.write_text(str(self.state.current_pid()))
|
|
59
|
+
self.effects.chmod(self.config.pid_path, 0o600)
|
|
60
|
+
level = self.state.current_log_level()
|
|
61
|
+
source = self._log_level_source()
|
|
62
|
+
self.effects.stderr.write(
|
|
63
|
+
f"ciel-runtime router starting on {bind_host}:{self.config.port} "
|
|
64
|
+
f"(client base {self.config.client_base}, log level "
|
|
65
|
+
f"{self.config.log_level_names.get(level, level)}, source={source})\n"
|
|
66
|
+
)
|
|
67
|
+
self.effects.stderr.flush()
|
|
68
|
+
server = self.effects.server_factory(
|
|
69
|
+
(bind_host, self.config.port), self.config.handler
|
|
70
|
+
)
|
|
71
|
+
for line in self.effects.configure_web_endpoints(bind_host):
|
|
72
|
+
self.effects.stderr.write(f"{line}\n")
|
|
73
|
+
self.effects.stderr.flush()
|
|
74
|
+
self.effects.start_watchdog(server)
|
|
75
|
+
channel_thread = self.effects.thread_factory(
|
|
76
|
+
target=lambda: self.effects.start_channels(runtime_config),
|
|
77
|
+
daemon=True,
|
|
78
|
+
name="ca-router-channel-sse-start",
|
|
79
|
+
)
|
|
80
|
+
channel_thread.start()
|
|
81
|
+
try:
|
|
82
|
+
server.serve_forever()
|
|
83
|
+
finally:
|
|
84
|
+
self.effects.stop_channels(None)
|
|
85
|
+
try:
|
|
86
|
+
self.config.pid_path.unlink()
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _log_level_source(self) -> str:
|
|
91
|
+
if self.config.log_level_path.exists():
|
|
92
|
+
return "file"
|
|
93
|
+
if self.state.env_value("CIEL_RUNTIME_LOG_LEVEL"):
|
|
94
|
+
return "env"
|
|
95
|
+
return "default"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"RouterServerConfig",
|
|
100
|
+
"RouterServerEffects",
|
|
101
|
+
"RouterServerRuntime",
|
|
102
|
+
"RouterServerStatePorts",
|
|
103
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Local router slash-command short-circuit controllers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable, Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShortcutHandler(Protocol):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ShortcutResponsePorts:
|
|
16
|
+
load_config: Callable[[], dict[str, Any]]
|
|
17
|
+
current_alias: Callable[[dict[str, Any]], str]
|
|
18
|
+
current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
|
|
19
|
+
write_anthropic: Callable[..., Any]
|
|
20
|
+
publish_event: Callable[..., Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ShortcutPredicates:
|
|
25
|
+
router_debug: Callable[[dict[str, Any]], bool]
|
|
26
|
+
version: Callable[[dict[str, Any]], bool]
|
|
27
|
+
channel_clear: Callable[[dict[str, Any]], bool]
|
|
28
|
+
live_llm_options: Callable[[dict[str, Any]], bool]
|
|
29
|
+
live_api_keys: Callable[[dict[str, Any]], bool]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class RouterDebugShortcutPorts:
|
|
34
|
+
value: Callable[[dict[str, Any]], str]
|
|
35
|
+
external_enabled: Callable[[dict[str, Any]], bool]
|
|
36
|
+
bind_host: Callable[[dict[str, Any]], str]
|
|
37
|
+
set_external: Callable[[Any], list[str]]
|
|
38
|
+
schedule_restart: Callable[[], Any]
|
|
39
|
+
version: str
|
|
40
|
+
source_fingerprint: str
|
|
41
|
+
config_dir: Path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class ChannelShortcutPorts:
|
|
46
|
+
value: Callable[[dict[str, Any]], str]
|
|
47
|
+
clear: Callable[[], dict[str, Any]]
|
|
48
|
+
status: Callable[[], dict[str, Any]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class LiveConfigShortcutPorts:
|
|
53
|
+
llm_value: Callable[[dict[str, Any]], str]
|
|
54
|
+
handle_llm: Callable[[str], tuple[list[str], bool]]
|
|
55
|
+
api_key_value: Callable[[dict[str, Any]], str]
|
|
56
|
+
handle_api_keys: Callable[[str], tuple[list[str], bool]]
|
|
57
|
+
api_key_count: Callable[[str, dict[str, Any]], int]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class RouterShortcutController:
|
|
62
|
+
response: ShortcutResponsePorts
|
|
63
|
+
predicates: ShortcutPredicates
|
|
64
|
+
debug: RouterDebugShortcutPorts
|
|
65
|
+
channel: ChannelShortcutPorts
|
|
66
|
+
live: LiveConfigShortcutPorts
|
|
67
|
+
|
|
68
|
+
def _model_and_stream(self, body: dict[str, Any]) -> tuple[str, bool]:
|
|
69
|
+
config = self.response.load_config()
|
|
70
|
+
return (
|
|
71
|
+
str(body.get("model") or self.response.current_alias(config)),
|
|
72
|
+
bool(body.get("stream", True)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def handle_router_debug(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
|
|
76
|
+
if not self.predicates.router_debug(body):
|
|
77
|
+
return False
|
|
78
|
+
config = self.response.load_config()
|
|
79
|
+
current = self.debug.external_enabled(config)
|
|
80
|
+
value = self.debug.value(body).strip().lower()
|
|
81
|
+
should_restart = False
|
|
82
|
+
if value in {"", "status", "state", "show", "?"}:
|
|
83
|
+
lines = [
|
|
84
|
+
f"Router debug external access: {'on' if current else 'off'}.",
|
|
85
|
+
f"Current router bind host: {self.debug.bind_host(config)}.",
|
|
86
|
+
]
|
|
87
|
+
elif value in {"toggle", "tog", "switch"}:
|
|
88
|
+
lines = self.debug.set_external(not current)
|
|
89
|
+
should_restart = True
|
|
90
|
+
elif value in {"on", "true", "1", "enable", "enabled"}:
|
|
91
|
+
lines = self.debug.set_external(True)
|
|
92
|
+
should_restart = True
|
|
93
|
+
elif value in {"off", "false", "0", "disable", "disabled"}:
|
|
94
|
+
lines = self.debug.set_external(False)
|
|
95
|
+
should_restart = True
|
|
96
|
+
else:
|
|
97
|
+
lines = [
|
|
98
|
+
"Usage: `/router-debug`, `/router-debug on`, `/router-debug off`, or `/router-debug status`."
|
|
99
|
+
]
|
|
100
|
+
if should_restart:
|
|
101
|
+
lines.append("Router restart scheduled so the bind address changes immediately.")
|
|
102
|
+
model, stream = self._model_and_stream(body)
|
|
103
|
+
self.response.write_anthropic(handler, model, "\n".join(lines), stream)
|
|
104
|
+
if should_restart:
|
|
105
|
+
self.debug.schedule_restart()
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def handle_version(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
|
|
109
|
+
if not self.predicates.version(body):
|
|
110
|
+
return False
|
|
111
|
+
lines = [
|
|
112
|
+
f"ciel-runtime {self.debug.version}",
|
|
113
|
+
f"source: {self.debug.source_fingerprint[:12]}",
|
|
114
|
+
f"config dir: {self.debug.config_dir}",
|
|
115
|
+
]
|
|
116
|
+
model, stream = self._model_and_stream(body)
|
|
117
|
+
self.response.write_anthropic(handler, model, "\n".join(lines), stream)
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def channel_status_lines(stats: dict[str, Any], cleared: bool) -> list[str]:
|
|
122
|
+
if cleared:
|
|
123
|
+
return [
|
|
124
|
+
"Ciel Runtime channel backlog discarded.",
|
|
125
|
+
f"- chat tail: {stats.get('chat_tail')}",
|
|
126
|
+
f"- LLM cursor advanced by: {stats.get('discarded_llm')}",
|
|
127
|
+
f"- MCP cursor advanced by: {stats.get('discarded_mcp')}",
|
|
128
|
+
f"- active MCP channel sessions updated: {stats.get('mcp_sessions_updated')}",
|
|
129
|
+
"New channel events arriving after this point will still be delivered.",
|
|
130
|
+
]
|
|
131
|
+
return [
|
|
132
|
+
"Ciel Runtime channel backlog status.",
|
|
133
|
+
f"- chat tail: {stats.get('chat_tail')}",
|
|
134
|
+
f"- pending LLM items by id range: {stats.get('pending_llm')}",
|
|
135
|
+
f"- pending MCP items by id range: {stats.get('pending_mcp')}",
|
|
136
|
+
f"- active MCP channel sessions: {stats.get('mcp_sessions')}",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
def handle_channel_clear(self, handler: ShortcutHandler, body: dict[str, Any]) -> bool:
|
|
140
|
+
if not self.predicates.channel_clear(body):
|
|
141
|
+
return False
|
|
142
|
+
value = self.channel.value(body).strip().lower()
|
|
143
|
+
if value in {"", "all", "clear", "discard", "drop", "purge", "reset", "now"}:
|
|
144
|
+
lines = self.channel_status_lines(self.channel.clear(), cleared=True)
|
|
145
|
+
elif value in {"status", "state", "show", "?", "dry-run", "dryrun"}:
|
|
146
|
+
lines = self.channel_status_lines(self.channel.status(), cleared=False)
|
|
147
|
+
else:
|
|
148
|
+
lines = ["Usage: `/channel-clear`, `/channel-clear all`, or `/channel-clear status`."]
|
|
149
|
+
model, stream = self._model_and_stream(body)
|
|
150
|
+
self.response.write_anthropic(handler, model, "\n".join(lines), stream)
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
def handle_live_llm_options(
|
|
154
|
+
self, handler: ShortcutHandler, body: dict[str, Any]
|
|
155
|
+
) -> bool:
|
|
156
|
+
if not self.predicates.live_llm_options(body):
|
|
157
|
+
return False
|
|
158
|
+
value = self.live.llm_value(body)
|
|
159
|
+
lines, changed = self.live.handle_llm(value)
|
|
160
|
+
if changed:
|
|
161
|
+
provider = self.response.current_provider(self.response.load_config())[0]
|
|
162
|
+
self.response.publish_event(
|
|
163
|
+
level="info",
|
|
164
|
+
category="config.llm",
|
|
165
|
+
message="live LLM options updated from slash command",
|
|
166
|
+
provider=provider,
|
|
167
|
+
data={"value": value},
|
|
168
|
+
)
|
|
169
|
+
model, stream = self._model_and_stream(body)
|
|
170
|
+
self.response.write_anthropic(handler, model, "\n".join(lines), stream)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def handle_live_api_keys(
|
|
174
|
+
self, handler: ShortcutHandler, body: dict[str, Any]
|
|
175
|
+
) -> bool:
|
|
176
|
+
if not self.predicates.live_api_keys(body):
|
|
177
|
+
return False
|
|
178
|
+
value = self.live.api_key_value(body)
|
|
179
|
+
lines, changed = self.live.handle_api_keys(value)
|
|
180
|
+
if changed:
|
|
181
|
+
provider, config = self.response.current_provider(self.response.load_config())
|
|
182
|
+
self.response.publish_event(
|
|
183
|
+
level="info",
|
|
184
|
+
category="config.api_key",
|
|
185
|
+
message="live API key settings updated from slash command",
|
|
186
|
+
provider=provider,
|
|
187
|
+
data={"key_count": self.live.api_key_count(provider, config)},
|
|
188
|
+
)
|
|
189
|
+
model, stream = self._model_and_stream(body)
|
|
190
|
+
self.response.write_anthropic(handler, model, "\n".join(lines), stream)
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = [
|
|
195
|
+
"ChannelShortcutPorts",
|
|
196
|
+
"LiveConfigShortcutPorts",
|
|
197
|
+
"RouterDebugShortcutPorts",
|
|
198
|
+
"RouterShortcutController",
|
|
199
|
+
"ShortcutPredicates",
|
|
200
|
+
"ShortcutResponsePorts",
|
|
201
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Deterministic provider/model fallback planning policies.
|
|
2
|
+
|
|
3
|
+
The policy decides *what may be tried*; transports remain responsible for
|
|
4
|
+
executing requests. This separation prevents retry mechanics, credentials,
|
|
5
|
+
and provider wire formats from becoming one stateful conditional block.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Iterable, Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FallbackReason = Literal["rate_limit", "unavailable", "timeout", "model_error"]
|
|
15
|
+
DEFAULT_FALLBACK_REASONS: frozenset[FallbackReason] = frozenset(
|
|
16
|
+
{"rate_limit", "unavailable", "timeout"}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class RouteTarget:
|
|
22
|
+
provider: str
|
|
23
|
+
model: str
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def identity(self) -> tuple[str, str]:
|
|
27
|
+
return (self.provider.strip().lower().replace("_", "-"), self.model.strip())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class FallbackRule:
|
|
32
|
+
target: RouteTarget
|
|
33
|
+
reasons: frozenset[FallbackReason] = DEFAULT_FALLBACK_REASONS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class FallbackPlan:
|
|
38
|
+
primary: RouteTarget
|
|
39
|
+
rules: tuple[FallbackRule, ...] = ()
|
|
40
|
+
|
|
41
|
+
def candidates(self, reason: FallbackReason) -> tuple[RouteTarget, ...]:
|
|
42
|
+
seen: set[tuple[str, str]] = set()
|
|
43
|
+
selected: list[RouteTarget] = []
|
|
44
|
+
for target in (self.primary, *(rule.target for rule in self.rules if reason in rule.reasons)):
|
|
45
|
+
if not target.provider.strip() or not target.model.strip() or target.identity in seen:
|
|
46
|
+
continue
|
|
47
|
+
seen.add(target.identity)
|
|
48
|
+
selected.append(target)
|
|
49
|
+
return tuple(selected)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_fallback_plan(
|
|
53
|
+
provider: str,
|
|
54
|
+
model: str,
|
|
55
|
+
configured: Iterable[dict[str, object]] = (),
|
|
56
|
+
) -> FallbackPlan:
|
|
57
|
+
"""Parse a small configuration surface into an immutable routing plan."""
|
|
58
|
+
|
|
59
|
+
rules: list[FallbackRule] = []
|
|
60
|
+
valid_reasons = {"rate_limit", "unavailable", "timeout", "model_error"}
|
|
61
|
+
for item in configured:
|
|
62
|
+
target_provider = str(item.get("provider") or provider).strip()
|
|
63
|
+
target_model = str(item.get("model") or "").strip()
|
|
64
|
+
raw_reasons = item.get("reasons")
|
|
65
|
+
values = raw_reasons if isinstance(raw_reasons, (list, tuple, set, frozenset)) else ()
|
|
66
|
+
reasons = frozenset(str(value) for value in values if str(value) in valid_reasons)
|
|
67
|
+
rules.append(
|
|
68
|
+
FallbackRule(
|
|
69
|
+
RouteTarget(target_provider, target_model),
|
|
70
|
+
reasons=reasons or DEFAULT_FALLBACK_REASONS,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return FallbackPlan(RouteTarget(provider, model), tuple(rules))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Atomic repository for router and context activity snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class RuntimeActivityPaths:
|
|
13
|
+
router: Path
|
|
14
|
+
context_compact: Path
|
|
15
|
+
context_usage: Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class RuntimeActivityClock:
|
|
20
|
+
epoch: Callable[[], float]
|
|
21
|
+
display: Callable[[], str]
|
|
22
|
+
temporary_suffix: Callable[[], str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class RuntimeActivityEffects:
|
|
27
|
+
publish: Callable[..., Any]
|
|
28
|
+
log: Callable[[str, str], None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class RuntimeActivityRepository:
|
|
33
|
+
paths: RuntimeActivityPaths
|
|
34
|
+
clock: RuntimeActivityClock
|
|
35
|
+
effects: RuntimeActivityEffects
|
|
36
|
+
|
|
37
|
+
def router_activity(
|
|
38
|
+
self,
|
|
39
|
+
event: str,
|
|
40
|
+
provider: str,
|
|
41
|
+
model: str | None = None,
|
|
42
|
+
**fields: Any,
|
|
43
|
+
) -> None:
|
|
44
|
+
try:
|
|
45
|
+
level = "error" if event == "error" else ("warn" if event in {"retry", "wait"} else "info")
|
|
46
|
+
self.effects.publish(
|
|
47
|
+
level=level,
|
|
48
|
+
category=f"router.{event}",
|
|
49
|
+
message=f"{event} {provider} {model or ''}".strip(),
|
|
50
|
+
provider=provider,
|
|
51
|
+
model=model,
|
|
52
|
+
data=fields,
|
|
53
|
+
)
|
|
54
|
+
self._write(
|
|
55
|
+
self.paths.router,
|
|
56
|
+
{"event": event, "provider": provider, "model": model or "", **fields},
|
|
57
|
+
)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
self._report_failure("router", exc)
|
|
60
|
+
|
|
61
|
+
def context_compact(
|
|
62
|
+
self,
|
|
63
|
+
provider: str,
|
|
64
|
+
model: str | None = None,
|
|
65
|
+
**fields: Any,
|
|
66
|
+
) -> None:
|
|
67
|
+
try:
|
|
68
|
+
self._write(
|
|
69
|
+
self.paths.context_compact,
|
|
70
|
+
{"event": "compact", "provider": provider, "model": model or "", **fields},
|
|
71
|
+
)
|
|
72
|
+
self.effects.publish(
|
|
73
|
+
level="info",
|
|
74
|
+
category="context.compact",
|
|
75
|
+
message=f"compact {provider} {model or ''}".strip(),
|
|
76
|
+
provider=provider,
|
|
77
|
+
model=model,
|
|
78
|
+
data=fields,
|
|
79
|
+
)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
self._report_failure("context_compact", exc)
|
|
82
|
+
|
|
83
|
+
def context_usage(
|
|
84
|
+
self,
|
|
85
|
+
provider: str,
|
|
86
|
+
provider_config: dict[str, Any],
|
|
87
|
+
body: dict[str, Any],
|
|
88
|
+
source: str,
|
|
89
|
+
*,
|
|
90
|
+
estimate_tokens: Callable[[dict[str, Any]], int],
|
|
91
|
+
context_limit: Callable[[str, dict[str, Any]], int | None],
|
|
92
|
+
) -> None:
|
|
93
|
+
try:
|
|
94
|
+
tokens = estimate_tokens(body)
|
|
95
|
+
limit = context_limit(provider, provider_config)
|
|
96
|
+
percent = round((tokens / limit) * 100.0, 1) if limit else None
|
|
97
|
+
model = str(body.get("model") or provider_config.get("current_model") or "")
|
|
98
|
+
details = {
|
|
99
|
+
"source": source,
|
|
100
|
+
"tokens": tokens,
|
|
101
|
+
"context_limit": limit,
|
|
102
|
+
"percent": percent,
|
|
103
|
+
"messages": len(body.get("messages") or []),
|
|
104
|
+
"tools": len(body.get("tools") or []),
|
|
105
|
+
}
|
|
106
|
+
self.effects.publish(
|
|
107
|
+
level="debug",
|
|
108
|
+
category="context.usage",
|
|
109
|
+
message=f"context usage {tokens}/{limit or '?'} tokens",
|
|
110
|
+
provider=provider,
|
|
111
|
+
model=model,
|
|
112
|
+
data=details,
|
|
113
|
+
)
|
|
114
|
+
self._write(self.paths.context_usage, {"provider": provider, "model": model, **details})
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
self._report_failure("context_usage", exc)
|
|
117
|
+
|
|
118
|
+
def _write(self, path: Path, data: dict[str, Any]) -> None:
|
|
119
|
+
record = {"updated_at": self.clock.epoch(), "time": self.clock.display(), **data}
|
|
120
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
temporary = path.with_name(f"{path.name}.{self.clock.temporary_suffix()}.tmp")
|
|
122
|
+
temporary.write_text(
|
|
123
|
+
json.dumps(record, ensure_ascii=False, separators=(",", ":")),
|
|
124
|
+
encoding="utf-8",
|
|
125
|
+
)
|
|
126
|
+
temporary.replace(path)
|
|
127
|
+
|
|
128
|
+
def _report_failure(self, operation: str, exc: BaseException) -> None:
|
|
129
|
+
try:
|
|
130
|
+
self.effects.log(
|
|
131
|
+
"WARN",
|
|
132
|
+
f"runtime_activity_{operation}_failed error={type(exc).__name__}: {exc}",
|
|
133
|
+
)
|
|
134
|
+
except Exception:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"RuntimeActivityClock",
|
|
140
|
+
"RuntimeActivityEffects",
|
|
141
|
+
"RuntimeActivityPaths",
|
|
142
|
+
"RuntimeActivityRepository",
|
|
143
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Concrete runtime adapter used while legacy launchers are incrementally decomposed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
from .architecture import LaunchSpec, RuntimeAdapter, RuntimeCommand
|
|
10
|
+
from .registry import AdapterRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CliRuntimeAdapter(RuntimeAdapter):
|
|
15
|
+
"""Materialize an already-normalized CLI launch through the runtime contract."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
executable: str
|
|
19
|
+
environment: Mapping[str, str] = field(default_factory=dict)
|
|
20
|
+
channel_injection: bool = False
|
|
21
|
+
|
|
22
|
+
def find_executable(self) -> Path | None:
|
|
23
|
+
return Path(self.executable) if self.executable else None
|
|
24
|
+
|
|
25
|
+
def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
|
|
26
|
+
executable = spec.runtime.executable or self.executable
|
|
27
|
+
if not executable:
|
|
28
|
+
raise RuntimeError(f"{self.name} runtime executable is unavailable")
|
|
29
|
+
prefix = spec.runtime.options.get("prefix_args", ())
|
|
30
|
+
prefix_args = tuple(str(value) for value in prefix) if isinstance(prefix, (list, tuple)) else ()
|
|
31
|
+
return RuntimeCommand(
|
|
32
|
+
argv=(str(executable), *prefix_args, *spec.passthrough),
|
|
33
|
+
env=dict(self.environment),
|
|
34
|
+
cwd=spec.cwd,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def mcp_config_paths(self, spec: LaunchSpec) -> tuple[Path, ...]:
|
|
38
|
+
return spec.runtime.mcp_config_paths
|
|
39
|
+
|
|
40
|
+
def supports_channel_injection(self, spec: LaunchSpec) -> bool:
|
|
41
|
+
return bool(self.channel_injection and spec.runtime.enable_channels)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ClaudeRuntimeAdapter(CliRuntimeAdapter):
|
|
45
|
+
def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
|
|
46
|
+
options = spec.runtime.options
|
|
47
|
+
argv = [str(spec.runtime.executable or self.executable), "--dangerously-skip-permissions"]
|
|
48
|
+
if options.get("bypass_permission_mode"):
|
|
49
|
+
argv.extend(("--permission-mode", "bypassPermissions"))
|
|
50
|
+
disallowed = str(options.get("disallowed_tools") or "")
|
|
51
|
+
if disallowed:
|
|
52
|
+
argv.extend(("--disallowedTools", disallowed))
|
|
53
|
+
model = str(options.get("model") or "")
|
|
54
|
+
if model:
|
|
55
|
+
argv.extend(("--model", model))
|
|
56
|
+
argv.extend(str(value) for value in options.get("extra_args", ()))
|
|
57
|
+
if options.get("passthrough_boundary"):
|
|
58
|
+
argv.append("--")
|
|
59
|
+
argv.extend(spec.passthrough)
|
|
60
|
+
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CodexRuntimeAdapter(CliRuntimeAdapter):
|
|
64
|
+
def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
|
|
65
|
+
options = spec.runtime.options
|
|
66
|
+
argv = [str(spec.runtime.executable or self.executable)]
|
|
67
|
+
argv.extend(str(value) for value in options.get("yolo_args", ()))
|
|
68
|
+
if spec.mode == "native":
|
|
69
|
+
argv.extend(str(value) for value in options.get("model_args", ()))
|
|
70
|
+
elif spec.mode == "routed":
|
|
71
|
+
argv.extend(str(value) for value in options.get("routed_config_args", ()))
|
|
72
|
+
argv.extend(str(value) for value in options.get("model_args", ()))
|
|
73
|
+
else:
|
|
74
|
+
argv.extend(str(value) for value in options.get("router_config_args", ()))
|
|
75
|
+
if spec.mode != "native":
|
|
76
|
+
argv.extend(str(value) for value in options.get("model_catalog_args", ()))
|
|
77
|
+
for key in ("alternate_screen_args", "mcp_args", "model_alias_args"):
|
|
78
|
+
argv.extend(str(value) for value in options.get(key, ()))
|
|
79
|
+
argv.extend(spec.passthrough)
|
|
80
|
+
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AgyRuntimeAdapter(CliRuntimeAdapter):
|
|
84
|
+
def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
|
|
85
|
+
options = spec.runtime.options
|
|
86
|
+
argv = [str(spec.runtime.executable or self.executable)]
|
|
87
|
+
argv.extend(str(value) for value in options.get("dangerous_args", ()))
|
|
88
|
+
argv.extend(spec.passthrough)
|
|
89
|
+
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
RUNTIME_ADAPTERS: AdapterRegistry[RuntimeAdapter] = AdapterRegistry()
|
|
93
|
+
RUNTIME_ADAPTERS.register("claude", lambda **kwargs: ClaudeRuntimeAdapter(name="claude", **kwargs))
|
|
94
|
+
RUNTIME_ADAPTERS.register("codex", lambda **kwargs: CodexRuntimeAdapter(name="codex", **kwargs))
|
|
95
|
+
RUNTIME_ADAPTERS.register("agy", lambda **kwargs: AgyRuntimeAdapter(name="agy", **kwargs))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"RUNTIME_ADAPTERS",
|
|
100
|
+
"AgyRuntimeAdapter",
|
|
101
|
+
"ClaudeRuntimeAdapter",
|
|
102
|
+
"CliRuntimeAdapter",
|
|
103
|
+
"CodexRuntimeAdapter",
|
|
104
|
+
]
|