@oneciel-ai/ciel-runtime 0.2.37 → 0.2.38
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/CHANGELOG.md +78 -0
- package/README.md +5 -2
- package/ciel_runtime.py +94 -96
- package/ciel_runtime_support/anthropic_model_policy.py +20 -3
- package/ciel_runtime_support/architecture.py +5 -0
- package/ciel_runtime_support/channel_inflight.py +4 -4
- package/ciel_runtime_support/channel_injection.py +56 -1
- package/ciel_runtime_support/channel_mcp_tools.py +34 -0
- package/ciel_runtime_support/channel_message_policy.py +9 -1
- package/ciel_runtime_support/channel_message_prompt.py +23 -0
- package/ciel_runtime_support/channel_pending_injection.py +86 -32
- package/ciel_runtime_support/channel_pending_poll.py +6 -2
- package/ciel_runtime_support/channel_runtime_environment.py +0 -11
- package/ciel_runtime_support/channel_terminal_context.py +0 -6
- package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
- package/ciel_runtime_support/channel_transcript_repository.py +16 -1
- package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
- package/ciel_runtime_support/channel_wake_context.py +6 -13
- package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
- package/ciel_runtime_support/chat_http_controller.py +88 -5
- package/ciel_runtime_support/claude_environment.py +100 -3
- package/ciel_runtime_support/claude_launch_assembly.py +4 -0
- package/ciel_runtime_support/claude_session_socket.py +181 -0
- package/ciel_runtime_support/cli_application_context.py +18 -10
- package/ciel_runtime_support/cli_dispatch.py +19 -3
- package/ciel_runtime_support/cli_parser.py +2 -0
- package/ciel_runtime_support/cli_usage.py +3 -2
- package/ciel_runtime_support/codex_backend_context.py +8 -3
- package/ciel_runtime_support/codex_completion_gate.py +198 -0
- package/ciel_runtime_support/codex_turn_recovery.py +240 -83
- package/ciel_runtime_support/compatibility_protocol.py +5 -2
- package/ciel_runtime_support/config_migrations.py +79 -0
- package/ciel_runtime_support/config_repository.py +7 -0
- package/ciel_runtime_support/context_summary_policy.py +24 -4
- package/ciel_runtime_support/external_event_menu.py +107 -0
- package/ciel_runtime_support/external_event_receiver.py +19 -1
- package/ciel_runtime_support/launch_state.py +2 -0
- package/ciel_runtime_support/managed_tool_injection.py +33 -0
- package/ciel_runtime_support/muse_runtime_context.py +291 -0
- package/ciel_runtime_support/ollama_thinking.py +12 -26
- package/ciel_runtime_support/prelaunch.py +13 -0
- package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
- package/ciel_runtime_support/prompt_compaction.py +39 -0
- package/ciel_runtime_support/provider_files_proxy.py +248 -0
- package/ciel_runtime_support/provider_model_identity.py +10 -2
- package/ciel_runtime_support/provider_option_cli.py +1 -1
- package/ciel_runtime_support/provider_request_access.py +54 -1
- package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
- package/ciel_runtime_support/providers/alibaba.py +34 -1
- package/ciel_runtime_support/providers/anthropic.py +18 -1
- package/ciel_runtime_support/providers/meta.py +252 -21
- package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
- package/ciel_runtime_support/remote_bridge.py +4 -0
- package/ciel_runtime_support/remote_instructions.py +5 -0
- package/ciel_runtime_support/remote_memory.py +1 -1
- package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
- package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
- package/ciel_runtime_support/responses_input_compatibility.py +19 -3
- package/ciel_runtime_support/router_http.py +301 -4
- package/ciel_runtime_support/router_observability_context.py +26 -0
- package/ciel_runtime_support/router_server_context.py +1 -0
- package/ciel_runtime_support/runtime_adapters.py +31 -0
- package/ciel_runtime_support/runtime_constants.py +23 -1
- package/ciel_runtime_support/runtime_input_gateway.py +85 -5
- package/ciel_runtime_support/runtime_input_status.py +150 -0
- package/ciel_runtime_support/runtime_launch.py +27 -4
- package/ciel_runtime_support/runtime_paths.py +1 -0
- package/ciel_runtime_support/speech_http_controller.py +3 -2
- package/ciel_runtime_support/streaming_anthropic.py +3 -0
- package/ciel_runtime_support/tool_call_events.py +98 -0
- package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
- package/ciel_runtime_support/ui_text.py +1 -0
- package/ciel_runtime_support/web_search_result_events.py +130 -0
- package/ciel_runtime_support/web_ui.py +1 -0
- package/ciel_runtime_support/windows_conpty.py +92 -8
- package/ciel_runtime_support/workspace_mcp.py +4 -1
- package/ciel_runtime_support/workspace_state.py +1 -0
- package/docs/Configuration.md +20 -4
- package/docs/MCP-Channels.md +79 -6
- package/docs/Managed-Tool-Injection.md +25 -0
- package/docs/Module-Map.md +4 -0
- package/docs/Muse-Code.md +73 -0
- package/docs/Observability.md +39 -0
- package/docs/Providers.md +29 -2
- package/docs/Remote-Bridge.md +12 -0
- package/docs/Router.md +1 -0
- package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
- package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
- package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
- package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
- package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
- package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
- package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
- package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
- package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
- package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
- package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
- package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
- package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
- package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
- package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
- package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
- package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
- package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
- package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
- package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
- package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
- package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
- package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
- package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
- package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
- package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
- package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
- package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
- package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
- package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
- package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
- package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
- package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
- package/package.json +1 -1
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
6
8
|
import json
|
|
7
9
|
import sys
|
|
10
|
+
import tempfile
|
|
8
11
|
import traceback
|
|
9
12
|
import urllib.error
|
|
10
13
|
import urllib.parse
|
|
@@ -30,6 +33,11 @@ from ciel_runtime_support.responses_input_compatibility import (
|
|
|
30
33
|
repair_replayed_response_items,
|
|
31
34
|
)
|
|
32
35
|
from ciel_runtime_support.channel_llm_context import ChannelLlmInjectionDeferred
|
|
36
|
+
from ciel_runtime_support.codex_completion_gate import (
|
|
37
|
+
ResponsesCompletionObservation,
|
|
38
|
+
completion_check_body,
|
|
39
|
+
request_requires_completion_check,
|
|
40
|
+
)
|
|
33
41
|
from ciel_runtime_support.request_body_policy import (
|
|
34
42
|
RequestBodyCapacityExceeded,
|
|
35
43
|
RequestBodyTooLarge,
|
|
@@ -63,6 +71,17 @@ class UpstreamContextExceeded(Exception):
|
|
|
63
71
|
self.payload = payload
|
|
64
72
|
|
|
65
73
|
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class _BufferedCodexResponse:
|
|
76
|
+
status: int
|
|
77
|
+
headers: Any
|
|
78
|
+
stream: Any
|
|
79
|
+
observation: ResponsesCompletionObservation
|
|
80
|
+
|
|
81
|
+
def close(self) -> None:
|
|
82
|
+
self.stream.close()
|
|
83
|
+
|
|
84
|
+
|
|
66
85
|
@dataclass(frozen=True, slots=True)
|
|
67
86
|
class CodexRoutedHeaderPolicy:
|
|
68
87
|
decorate: Callable[[dict[str, str]], dict[str, str]]
|
|
@@ -154,6 +173,13 @@ class RouterHttpErrors:
|
|
|
154
173
|
try_write_json: Callable[..., Any]
|
|
155
174
|
|
|
156
175
|
|
|
176
|
+
@dataclass(frozen=True, slots=True)
|
|
177
|
+
class RouterHttpFileEndpoints:
|
|
178
|
+
get: Callable[[Any, str, dict[str, Any]], bool]
|
|
179
|
+
post: Callable[[Any, str, int, str, dict[str, Any]], bool]
|
|
180
|
+
delete: Callable[[Any, str, dict[str, Any]], bool]
|
|
181
|
+
|
|
182
|
+
|
|
157
183
|
@dataclass(frozen=True, slots=True)
|
|
158
184
|
class RouterHttpServices:
|
|
159
185
|
core: RouterHttpCore
|
|
@@ -161,6 +187,7 @@ class RouterHttpServices:
|
|
|
161
187
|
post: RouterHttpPostEndpoints
|
|
162
188
|
presentation: RouterHttpPresentation
|
|
163
189
|
errors: RouterHttpErrors
|
|
190
|
+
files: RouterHttpFileEndpoints | None = None
|
|
164
191
|
|
|
165
192
|
|
|
166
193
|
@dataclass(frozen=True, slots=True)
|
|
@@ -216,7 +243,11 @@ class CodexBackendHttpAdapter:
|
|
|
216
243
|
def copy_response_headers(handler: BaseHTTPRequestHandler, headers: Any) -> None:
|
|
217
244
|
allowed = {
|
|
218
245
|
"cache-control",
|
|
246
|
+
"content-disposition",
|
|
247
|
+
"content-length",
|
|
219
248
|
"content-type",
|
|
249
|
+
"etag",
|
|
250
|
+
"last-modified",
|
|
220
251
|
"openai-request-id",
|
|
221
252
|
"request-id",
|
|
222
253
|
"retry-after",
|
|
@@ -524,9 +555,121 @@ class CodexBackendHttpAdapter:
|
|
|
524
555
|
)
|
|
525
556
|
self._retry.sleep(wait)
|
|
526
557
|
continue
|
|
527
|
-
|
|
558
|
+
if mutate_responses and upstream_body.get("tools"):
|
|
559
|
+
buffered = self._buffer_codex_response(response, preamble)
|
|
560
|
+
chosen = buffered
|
|
561
|
+
try:
|
|
562
|
+
if request_requires_completion_check(
|
|
563
|
+
upstream_body, buffered.observation
|
|
564
|
+
):
|
|
565
|
+
validated = self._validate_codex_completion(
|
|
566
|
+
provider,
|
|
567
|
+
config,
|
|
568
|
+
url,
|
|
569
|
+
headers,
|
|
570
|
+
upstream_body,
|
|
571
|
+
buffered.observation,
|
|
572
|
+
)
|
|
573
|
+
if validated is not None:
|
|
574
|
+
if validated.observation.completion_confirmed:
|
|
575
|
+
self._retry.log(
|
|
576
|
+
"INFO",
|
|
577
|
+
"codex_completion_gate_confirmed "
|
|
578
|
+
f"model={str(upstream_body.get('model') or '-')}",
|
|
579
|
+
)
|
|
580
|
+
elif (
|
|
581
|
+
validated.observation.status == "completed"
|
|
582
|
+
and validated.observation.has_action
|
|
583
|
+
):
|
|
584
|
+
chosen = validated
|
|
585
|
+
self._retry.log(
|
|
586
|
+
"WARN",
|
|
587
|
+
"codex_completion_gate_continued "
|
|
588
|
+
f"model={str(upstream_body.get('model') or '-')}",
|
|
589
|
+
)
|
|
590
|
+
self._write_buffered_response(handler, chosen)
|
|
591
|
+
if validated is not None and validated is not chosen:
|
|
592
|
+
validated.close()
|
|
593
|
+
else:
|
|
594
|
+
self._write_buffered_response(handler, chosen)
|
|
595
|
+
finally:
|
|
596
|
+
if chosen is not buffered:
|
|
597
|
+
chosen.close()
|
|
598
|
+
buffered.close()
|
|
599
|
+
else:
|
|
600
|
+
self._write_response(handler, response, preamble)
|
|
528
601
|
break
|
|
529
602
|
|
|
603
|
+
def _buffer_codex_response(
|
|
604
|
+
self, response: Any, preamble: Any
|
|
605
|
+
) -> _BufferedCodexResponse:
|
|
606
|
+
stream = tempfile.SpooledTemporaryFile(max_size=1024 * 1024)
|
|
607
|
+
observation = ResponsesCompletionObservation()
|
|
608
|
+
initial = bytes(getattr(preamble, "payload", b"") or b"")
|
|
609
|
+
if initial:
|
|
610
|
+
stream.write(initial)
|
|
611
|
+
observation.feed(initial)
|
|
612
|
+
while chunk := response.read(65_536):
|
|
613
|
+
stream.write(chunk)
|
|
614
|
+
observation.feed(chunk)
|
|
615
|
+
observation.finish()
|
|
616
|
+
stream.seek(0)
|
|
617
|
+
return _BufferedCodexResponse(
|
|
618
|
+
status=int(getattr(response, "status", 200)),
|
|
619
|
+
headers=response.headers,
|
|
620
|
+
stream=stream,
|
|
621
|
+
observation=observation,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def _validate_codex_completion(
|
|
625
|
+
self,
|
|
626
|
+
provider: str,
|
|
627
|
+
config: dict[str, Any],
|
|
628
|
+
url: str,
|
|
629
|
+
headers: dict[str, str],
|
|
630
|
+
upstream_body: dict[str, Any],
|
|
631
|
+
observation: ResponsesCompletionObservation,
|
|
632
|
+
) -> _BufferedCodexResponse | None:
|
|
633
|
+
validation_body = completion_check_body(upstream_body, observation)
|
|
634
|
+
validation_data = json.dumps(validation_body).encode("utf-8")
|
|
635
|
+
dump_upstream_request(url, validation_data, self._retry.log)
|
|
636
|
+
request = urllib.request.Request(
|
|
637
|
+
url, data=validation_data, headers=headers, method="POST"
|
|
638
|
+
)
|
|
639
|
+
try:
|
|
640
|
+
with self._open_with_transport_retry(
|
|
641
|
+
request,
|
|
642
|
+
provider,
|
|
643
|
+
config,
|
|
644
|
+
str(upstream_body.get("model") or ""),
|
|
645
|
+
"completion_check",
|
|
646
|
+
) as response:
|
|
647
|
+
preamble = self._retry.read_preamble(response)
|
|
648
|
+
if preamble is not None and (
|
|
649
|
+
preamble.capacity_error_code
|
|
650
|
+
or getattr(preamble, "context_error_code", None)
|
|
651
|
+
):
|
|
652
|
+
return None
|
|
653
|
+
return self._buffer_codex_response(response, preamble)
|
|
654
|
+
except Exception as exc: # noqa: BLE001 - preserve the original response
|
|
655
|
+
self._retry.log(
|
|
656
|
+
"WARN",
|
|
657
|
+
"codex_completion_gate_failed "
|
|
658
|
+
f"error={type(exc).__name__}: {exc}",
|
|
659
|
+
)
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
def _write_buffered_response(
|
|
663
|
+
self, handler: BaseHTTPRequestHandler, response: _BufferedCodexResponse
|
|
664
|
+
) -> None:
|
|
665
|
+
handler.send_response(response.status)
|
|
666
|
+
self.copy_response_headers(handler, response.headers)
|
|
667
|
+
handler.end_headers()
|
|
668
|
+
response.stream.seek(0)
|
|
669
|
+
while chunk := response.stream.read(65_536):
|
|
670
|
+
handler.wfile.write(chunk)
|
|
671
|
+
handler.wfile.flush()
|
|
672
|
+
|
|
530
673
|
def _open_with_transport_retry(
|
|
531
674
|
self,
|
|
532
675
|
request: urllib.request.Request,
|
|
@@ -613,6 +756,8 @@ class EventHttpPorts:
|
|
|
613
756
|
|
|
614
757
|
|
|
615
758
|
class EventHttpAdapter:
|
|
759
|
+
_WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
760
|
+
|
|
616
761
|
def __init__(self, ports: EventHttpPorts) -> None:
|
|
617
762
|
self._ports = ports
|
|
618
763
|
|
|
@@ -650,6 +795,8 @@ class EventHttpAdapter:
|
|
|
650
795
|
},
|
|
651
796
|
)
|
|
652
797
|
return True
|
|
798
|
+
if path == "/ca/events/ws":
|
|
799
|
+
return self._handle_websocket(handler, query)
|
|
653
800
|
if path != "/ca/events/stream":
|
|
654
801
|
return False
|
|
655
802
|
last_id = self.query_int(query, "after", 0)
|
|
@@ -659,11 +806,30 @@ class EventHttpAdapter:
|
|
|
659
806
|
handler.send_header("connection", "close")
|
|
660
807
|
handler.end_headers()
|
|
661
808
|
try:
|
|
662
|
-
|
|
809
|
+
level = (query.get("level") or [None])[0]
|
|
810
|
+
category = (query.get("category") or [None])[0]
|
|
811
|
+
last_id = self._write_events(
|
|
812
|
+
handler,
|
|
813
|
+
self._ports.recent(
|
|
814
|
+
limit=200,
|
|
815
|
+
min_id=last_id,
|
|
816
|
+
level=level,
|
|
817
|
+
category=category,
|
|
818
|
+
),
|
|
819
|
+
last_id,
|
|
820
|
+
)
|
|
663
821
|
while True:
|
|
664
|
-
|
|
822
|
+
observed = self._ports.wait_after(last_id, timeout=15.0)
|
|
823
|
+
observed_last_id = self._max_event_id(observed, last_id)
|
|
824
|
+
events = self._filtered(
|
|
825
|
+
observed,
|
|
826
|
+
level=level,
|
|
827
|
+
category=category,
|
|
828
|
+
)
|
|
665
829
|
if events:
|
|
666
830
|
last_id = self._write_events(handler, events, last_id)
|
|
831
|
+
if observed:
|
|
832
|
+
last_id = max(last_id, observed_last_id)
|
|
667
833
|
else:
|
|
668
834
|
handler.wfile.write(b": keepalive\n\n")
|
|
669
835
|
handler.wfile.flush()
|
|
@@ -673,6 +839,125 @@ class EventHttpAdapter:
|
|
|
673
839
|
self._ports.log("DEBUG", f"events stream closed: {type(exc).__name__}: {exc}")
|
|
674
840
|
return True
|
|
675
841
|
|
|
842
|
+
def _handle_websocket(
|
|
843
|
+
self,
|
|
844
|
+
handler: BaseHTTPRequestHandler,
|
|
845
|
+
query: dict[str, list[str]],
|
|
846
|
+
) -> bool:
|
|
847
|
+
upgrade = str(handler.headers.get("upgrade") or "").strip().lower()
|
|
848
|
+
key = str(handler.headers.get("sec-websocket-key") or "").strip()
|
|
849
|
+
version = str(handler.headers.get("sec-websocket-version") or "13").strip()
|
|
850
|
+
if upgrade != "websocket" or not key or version != "13":
|
|
851
|
+
self._ports.write_json(
|
|
852
|
+
handler,
|
|
853
|
+
{
|
|
854
|
+
"ok": False,
|
|
855
|
+
"error": "websocket_upgrade_required",
|
|
856
|
+
"required_version": "13",
|
|
857
|
+
},
|
|
858
|
+
400,
|
|
859
|
+
)
|
|
860
|
+
return True
|
|
861
|
+
accept = base64.b64encode(
|
|
862
|
+
hashlib.sha1((key + self._WEBSOCKET_GUID).encode("ascii")).digest()
|
|
863
|
+
).decode("ascii")
|
|
864
|
+
handler.protocol_version = "HTTP/1.1"
|
|
865
|
+
handler.send_response(101)
|
|
866
|
+
handler.send_header("upgrade", "websocket")
|
|
867
|
+
handler.send_header("connection", "Upgrade")
|
|
868
|
+
handler.send_header("sec-websocket-accept", accept)
|
|
869
|
+
handler.end_headers()
|
|
870
|
+
handler.close_connection = True
|
|
871
|
+
last_id = self.query_int(query, "after", 0)
|
|
872
|
+
level = (query.get("level") or [None])[0]
|
|
873
|
+
category = (query.get("category") or [None])[0]
|
|
874
|
+
try:
|
|
875
|
+
last_id = self._write_ws_events(
|
|
876
|
+
handler,
|
|
877
|
+
self._ports.recent(
|
|
878
|
+
limit=200,
|
|
879
|
+
min_id=last_id,
|
|
880
|
+
level=level,
|
|
881
|
+
category=category,
|
|
882
|
+
),
|
|
883
|
+
last_id,
|
|
884
|
+
)
|
|
885
|
+
while True:
|
|
886
|
+
observed = self._ports.wait_after(last_id, timeout=15.0)
|
|
887
|
+
observed_last_id = self._max_event_id(observed, last_id)
|
|
888
|
+
events = self._filtered(
|
|
889
|
+
observed,
|
|
890
|
+
level=level,
|
|
891
|
+
category=category,
|
|
892
|
+
)
|
|
893
|
+
if events:
|
|
894
|
+
last_id = self._write_ws_events(handler, events, last_id)
|
|
895
|
+
if observed:
|
|
896
|
+
last_id = max(last_id, observed_last_id)
|
|
897
|
+
else:
|
|
898
|
+
handler.wfile.write(self._websocket_frame(b"", opcode=0x9))
|
|
899
|
+
handler.wfile.flush()
|
|
900
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
901
|
+
return True
|
|
902
|
+
except Exception as exc:
|
|
903
|
+
self._ports.log("DEBUG", f"events websocket closed: {type(exc).__name__}: {exc}")
|
|
904
|
+
return True
|
|
905
|
+
|
|
906
|
+
@staticmethod
|
|
907
|
+
def _max_event_id(events: list[dict[str, Any]], default: int) -> int:
|
|
908
|
+
return max(
|
|
909
|
+
(int(event.get("id") or 0) for event in events),
|
|
910
|
+
default=default,
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
@staticmethod
|
|
914
|
+
def _filtered(
|
|
915
|
+
events: list[dict[str, Any]],
|
|
916
|
+
*,
|
|
917
|
+
level: str | None,
|
|
918
|
+
category: str | None,
|
|
919
|
+
) -> list[dict[str, Any]]:
|
|
920
|
+
if level:
|
|
921
|
+
ranks = {"trace": 10, "debug": 20, "info": 30, "warn": 40, "error": 50, "fatal": 60}
|
|
922
|
+
threshold = ranks.get(str(level).lower(), 30)
|
|
923
|
+
events = [
|
|
924
|
+
event
|
|
925
|
+
for event in events
|
|
926
|
+
if ranks.get(str(event.get("level") or "info").lower(), 30) >= threshold
|
|
927
|
+
]
|
|
928
|
+
if category:
|
|
929
|
+
events = [
|
|
930
|
+
event
|
|
931
|
+
for event in events
|
|
932
|
+
if str(event.get("category") or "").startswith(category)
|
|
933
|
+
]
|
|
934
|
+
return events
|
|
935
|
+
|
|
936
|
+
@staticmethod
|
|
937
|
+
def _websocket_frame(payload: bytes, *, opcode: int = 0x1) -> bytes:
|
|
938
|
+
size = len(payload)
|
|
939
|
+
head = bytes([0x80 | (opcode & 0x0F)])
|
|
940
|
+
if size < 126:
|
|
941
|
+
return head + bytes([size]) + payload
|
|
942
|
+
if size <= 0xFFFF:
|
|
943
|
+
return head + bytes([126]) + size.to_bytes(2, "big") + payload
|
|
944
|
+
return head + bytes([127]) + size.to_bytes(8, "big") + payload
|
|
945
|
+
|
|
946
|
+
def _write_ws_events(
|
|
947
|
+
self,
|
|
948
|
+
handler: BaseHTTPRequestHandler,
|
|
949
|
+
events: list[dict[str, Any]],
|
|
950
|
+
last_id: int,
|
|
951
|
+
) -> int:
|
|
952
|
+
for event in events:
|
|
953
|
+
last_id = max(last_id, int(event.get("id") or 0))
|
|
954
|
+
payload = json.dumps(event, ensure_ascii=False, separators=(",", ":")).encode(
|
|
955
|
+
"utf-8"
|
|
956
|
+
)
|
|
957
|
+
handler.wfile.write(self._websocket_frame(payload))
|
|
958
|
+
handler.wfile.flush()
|
|
959
|
+
return last_id
|
|
960
|
+
|
|
676
961
|
@staticmethod
|
|
677
962
|
def _write_events(
|
|
678
963
|
handler: BaseHTTPRequestHandler,
|
|
@@ -747,6 +1032,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
|
|
|
747
1032
|
return
|
|
748
1033
|
if services.core.reject_external(self, cfg):
|
|
749
1034
|
return
|
|
1035
|
+
if services.files is not None and services.files.get(self, path, cfg):
|
|
1036
|
+
return
|
|
750
1037
|
endpoints = services.get
|
|
751
1038
|
if endpoints.tui(self, path, query):
|
|
752
1039
|
return
|
|
@@ -1068,11 +1355,19 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
|
|
|
1068
1355
|
length = self._validated_content_length(path, services)
|
|
1069
1356
|
if length is None:
|
|
1070
1357
|
return
|
|
1358
|
+
content_type = str(
|
|
1359
|
+
self.headers.get("content-type") or "application/json"
|
|
1360
|
+
)
|
|
1361
|
+
if (
|
|
1362
|
+
services.files is not None
|
|
1363
|
+
and services.files.post(self, path, length, content_type, cfg)
|
|
1364
|
+
):
|
|
1365
|
+
return
|
|
1071
1366
|
try:
|
|
1072
1367
|
admission = services.core.request_body_policy.admit(
|
|
1073
1368
|
path,
|
|
1074
1369
|
length,
|
|
1075
|
-
|
|
1370
|
+
content_type,
|
|
1076
1371
|
)
|
|
1077
1372
|
with admission:
|
|
1078
1373
|
raw = self._read_request_body(path, services, length)
|
|
@@ -1292,6 +1587,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
|
|
|
1292
1587
|
cfg = services.core.load_config()
|
|
1293
1588
|
if services.core.reject_external(self, cfg):
|
|
1294
1589
|
return
|
|
1590
|
+
if services.files is not None and services.files.delete(self, path, cfg):
|
|
1591
|
+
return
|
|
1295
1592
|
services.presentation.write_json(
|
|
1296
1593
|
self,
|
|
1297
1594
|
{"type": "error", "error": {"type": "not_found_error", "message": path}},
|
|
@@ -71,6 +71,7 @@ class SseObservabilityPorts:
|
|
|
71
71
|
current_log_level: Callable[[], int]
|
|
72
72
|
truncate: Callable[[str, int], str]
|
|
73
73
|
log: Callable[[str, str], None]
|
|
74
|
+
event_publish: Callable[..., Any]
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
@dataclass(frozen=True, slots=True)
|
|
@@ -185,6 +186,31 @@ class RouterObservabilityContext:
|
|
|
185
186
|
|
|
186
187
|
def append_tool_call(self, *args: Any, **kwargs: Any) -> None:
|
|
187
188
|
self.sse_repository().append_tool_call(*args, **kwargs)
|
|
189
|
+
event = str(args[0] if args else kwargs.get("event") or "tool_call")
|
|
190
|
+
payload = args[1] if len(args) > 1 else kwargs.get("payload")
|
|
191
|
+
payload = payload if isinstance(payload, dict) else {}
|
|
192
|
+
provider = event.removesuffix("_stream_tool_call").replace("_", "-")
|
|
193
|
+
name = str(payload.get("matched_name") or payload.get("raw_name") or "tool").strip()
|
|
194
|
+
config = self.preview.load_config()
|
|
195
|
+
tool_event_config = config.get("tool_call_events")
|
|
196
|
+
tool_event_config = tool_event_config if isinstance(tool_event_config, dict) else {}
|
|
197
|
+
data = {
|
|
198
|
+
"call_id": str(payload.get("tool_id") or ""),
|
|
199
|
+
"name": name,
|
|
200
|
+
"call_type": event,
|
|
201
|
+
"sse_index": payload.get("sse_index"),
|
|
202
|
+
}
|
|
203
|
+
if bool(tool_event_config.get("include_arguments", True)):
|
|
204
|
+
data["arguments"] = payload.get("emitted_input")
|
|
205
|
+
self.sse.event_publish(
|
|
206
|
+
level="info",
|
|
207
|
+
category="tool.call",
|
|
208
|
+
message=f"{provider} tool call: {name}",
|
|
209
|
+
source="router-stream",
|
|
210
|
+
provider=provider,
|
|
211
|
+
model=str(payload.get("model") or ""),
|
|
212
|
+
data=data,
|
|
213
|
+
)
|
|
188
214
|
|
|
189
215
|
|
|
190
216
|
@dataclass(frozen=True, slots=True)
|
|
@@ -65,6 +65,7 @@ class RouterServerContext:
|
|
|
65
65
|
"chat": "/ca/chat/health",
|
|
66
66
|
"plan": "/ca/plan/artifacts",
|
|
67
67
|
"events": "/ca/events",
|
|
68
|
+
"events_websocket": "/ca/events/ws",
|
|
68
69
|
"external_event_receivers": "/ca/events/receivers",
|
|
69
70
|
"otlp_logs": "/v1/logs",
|
|
70
71
|
"telemetry_logs_tool": "POST /ca/mcp tools/call telemetry_logs",
|
|
@@ -116,12 +116,42 @@ class ZcodeRuntimeAdapter(CliRuntimeAdapter):
|
|
|
116
116
|
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
class MuseRuntimeAdapter(CliRuntimeAdapter):
|
|
120
|
+
"""Official Muse Code CLI with optional WSL command prefix."""
|
|
121
|
+
|
|
122
|
+
def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
|
|
123
|
+
options = spec.runtime.options
|
|
124
|
+
argv = [str(spec.runtime.executable or self.executable)]
|
|
125
|
+
argv.extend(str(value) for value in options.get("prefix_args", ()))
|
|
126
|
+
passthrough = list(spec.passthrough)
|
|
127
|
+
subcommand = passthrough[0] if passthrough else ""
|
|
128
|
+
utility_commands = {
|
|
129
|
+
"config", "export", "trace", "skills", "sandbox", "schema", "serve",
|
|
130
|
+
"session-message", "auth", "login", "logout", "init", "help", "version",
|
|
131
|
+
}
|
|
132
|
+
if subcommand in utility_commands or subcommand in {"--help", "-h", "--version", "-V", "-v"}:
|
|
133
|
+
argv.extend(passthrough)
|
|
134
|
+
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
135
|
+
if subcommand in {"exec", "resume"}:
|
|
136
|
+
argv.append(passthrough.pop(0))
|
|
137
|
+
argv.extend(str(value) for value in options.get("yolo_args", ()))
|
|
138
|
+
model = str(options.get("model") or "").strip()
|
|
139
|
+
if model:
|
|
140
|
+
argv.extend(("--model", model))
|
|
141
|
+
effort = str(options.get("reasoning_effort") or "").strip()
|
|
142
|
+
if effort:
|
|
143
|
+
argv.extend(("--reasoning-effort", effort))
|
|
144
|
+
argv.extend(passthrough)
|
|
145
|
+
return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
|
|
146
|
+
|
|
147
|
+
|
|
119
148
|
RUNTIME_ADAPTERS: AdapterRegistry[RuntimeAdapter] = AdapterRegistry()
|
|
120
149
|
RUNTIME_ADAPTERS.register("claude", lambda **kwargs: ClaudeRuntimeAdapter(name="claude", **kwargs))
|
|
121
150
|
RUNTIME_ADAPTERS.register("codex", lambda **kwargs: CodexRuntimeAdapter(name="codex", **kwargs))
|
|
122
151
|
RUNTIME_ADAPTERS.register("agy", lambda **kwargs: AgyRuntimeAdapter(name="agy", **kwargs))
|
|
123
152
|
RUNTIME_ADAPTERS.register("grok", lambda **kwargs: GrokRuntimeAdapter(name="grok", **kwargs))
|
|
124
153
|
RUNTIME_ADAPTERS.register("zcode", lambda **kwargs: ZcodeRuntimeAdapter(name="zcode", **kwargs))
|
|
154
|
+
RUNTIME_ADAPTERS.register("muse", lambda **kwargs: MuseRuntimeAdapter(name="muse", **kwargs))
|
|
125
155
|
|
|
126
156
|
|
|
127
157
|
__all__ = [
|
|
@@ -131,5 +161,6 @@ __all__ = [
|
|
|
131
161
|
"CliRuntimeAdapter",
|
|
132
162
|
"CodexRuntimeAdapter",
|
|
133
163
|
"GrokRuntimeAdapter",
|
|
164
|
+
"MuseRuntimeAdapter",
|
|
134
165
|
"ZcodeRuntimeAdapter",
|
|
135
166
|
]
|
|
@@ -12,7 +12,17 @@ ANTHROPIC_MODEL_DOCS_URLS = (
|
|
|
12
12
|
ANTHROPIC_MODEL_DOCS_URL,
|
|
13
13
|
"https://platform.claude.com/docs/en/about-claude/models/overview",
|
|
14
14
|
)
|
|
15
|
+
ANTHROPIC_ONE_MILLION_MODEL_IDS: tuple[str, ...] = (
|
|
16
|
+
"claude-fable-5-1[1m]",
|
|
17
|
+
"claude-fable-5[1m]",
|
|
18
|
+
"claude-opus-5[1m]",
|
|
19
|
+
"claude-opus-4-8[1m]",
|
|
20
|
+
"claude-sonnet-5[1m]",
|
|
21
|
+
"claude-sonnet-4-6[1m]",
|
|
22
|
+
)
|
|
15
23
|
ANTHROPIC_PUBLIC_MODEL_FALLBACK_IDS: tuple[str, ...] = (
|
|
24
|
+
*ANTHROPIC_ONE_MILLION_MODEL_IDS,
|
|
25
|
+
"claude-fable-5-1",
|
|
16
26
|
"claude-fable-5",
|
|
17
27
|
"claude-opus-5",
|
|
18
28
|
"claude-opus-4-8",
|
|
@@ -87,7 +97,7 @@ OPENCODE_ENDPOINT_ALIASES = {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
APP_NAME = "Ciel Runtime"
|
|
90
|
-
VERSION = "0.2.
|
|
100
|
+
VERSION = "0.2.38"
|
|
91
101
|
CREDITS = "Credits: One Ciel LLC"
|
|
92
102
|
PRELAUNCH_CANCEL = 10
|
|
93
103
|
PRELAUNCH_LAUNCH_CODEX = 11
|
|
@@ -175,12 +185,24 @@ ROUTED_CODEX_COMPAT_PROMPT = (
|
|
|
175
185
|
"You are running inside Codex CLI through the ciel-runtime router. Do not stop after announcing what you plan to do. "
|
|
176
186
|
"When the user asks you to inspect, create, edit, or run code, call the tools available in this turn and report the concrete result. "
|
|
177
187
|
"Never end a turn with only a statement of intent such as saying you will now start; perform the work in the same turn. "
|
|
188
|
+
"If you identify a concrete next action in a progress report, call the next tool before returning a final answer. "
|
|
189
|
+
"For a persistent monitoring goal, unchanged external state is a verified wait, not a blocker: do not mark the goal blocked merely because the next market, job, or service event has not occurred. "
|
|
190
|
+
"Poll a confirmed-live handle or use the available recurring monitor or wait mechanism, and leave the goal active while safe in-scope monitoring work remains. "
|
|
178
191
|
"If the task has several reasonable in-scope parts, do all of them; do not ask which part to start unless the user explicitly asked for a choice. "
|
|
179
192
|
"If you decide not to use tools, give the complete answer in the same turn. "
|
|
180
193
|
"Use exactly the tool schema provided and do not invent extra fields, and never write pseudo tool calls or partial JSON where a real tool call is required."
|
|
181
194
|
)
|
|
182
195
|
LANGUAGES = {"en": "English", "ko": "한국어", "ja": "日本語", "zh": "中文"}
|
|
183
196
|
MODEL_PRESETS: dict[str, dict[str, Any]] = {
|
|
197
|
+
# Muse Spark reasoning tokens consume the output budget. A live
|
|
198
|
+
# contributor probe exhausted 128 tokens before it could emit tool_use;
|
|
199
|
+
# 4096 completed the same request and is also the documented minimum used
|
|
200
|
+
# by Meta's long-media examples.
|
|
201
|
+
"muse-spark-1.3": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
|
|
202
|
+
"muse-spark-1.3-contributor": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
|
|
203
|
+
"muse-spark-1.2": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
|
|
204
|
+
"muse-spark-1.2-contributor": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
|
|
205
|
+
"muse-spark-1.1": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
|
|
184
206
|
"glm-5.3": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
|
|
185
207
|
"glm-5.3:cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
|
|
186
208
|
"deepseek-v4-flash:0731": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
|