@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
|
@@ -15,6 +15,8 @@ import urllib.error
|
|
|
15
15
|
import urllib.request
|
|
16
16
|
|
|
17
17
|
from .remote_instructions import expand_environment_references
|
|
18
|
+
from .tool_call_events import project_transcript_tool_calls
|
|
19
|
+
from .web_search_result_events import project_web_search_results
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
@dataclass(frozen=True, slots=True)
|
|
@@ -49,6 +51,34 @@ class TranscriptDeliverySettings:
|
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ToolCallEventSettings:
|
|
56
|
+
enabled: bool
|
|
57
|
+
poll_interval_seconds: float
|
|
58
|
+
max_batch_bytes: int
|
|
59
|
+
start_mode: str
|
|
60
|
+
include_arguments: bool
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_config(cls, config: dict[str, Any]) -> "ToolCallEventSettings":
|
|
64
|
+
raw = config.get("tool_call_events")
|
|
65
|
+
values = raw if isinstance(raw, dict) else {}
|
|
66
|
+
start_mode = str(values.get("start_mode") or "tail").strip().lower()
|
|
67
|
+
if start_mode not in {"tail", "beginning"}:
|
|
68
|
+
start_mode = "tail"
|
|
69
|
+
return cls(
|
|
70
|
+
enabled=bool(values.get("enabled", True)),
|
|
71
|
+
poll_interval_seconds=max(
|
|
72
|
+
0.1, min(60.0, float(values.get("poll_interval_ms") or 500) / 1000.0)
|
|
73
|
+
),
|
|
74
|
+
max_batch_bytes=max(
|
|
75
|
+
1024, min(16_777_216, int(values.get("max_batch_bytes") or 1_048_576))
|
|
76
|
+
),
|
|
77
|
+
start_mode=start_mode,
|
|
78
|
+
include_arguments=bool(values.get("include_arguments", True)),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
@dataclass(frozen=True, slots=True)
|
|
53
83
|
class TranscriptDeliveryPorts:
|
|
54
84
|
load_config: Callable[[], dict[str, Any]]
|
|
@@ -56,6 +86,8 @@ class TranscriptDeliveryPorts:
|
|
|
56
86
|
scope: Callable[[], dict[str, Any]]
|
|
57
87
|
log: Callable[[str, str], None]
|
|
58
88
|
epoch: Callable[[], float] = time.time
|
|
89
|
+
event_publish: Callable[..., Any] = lambda **_kwargs: None
|
|
90
|
+
event_recent: Callable[..., list[dict[str, Any]]] = lambda **_kwargs: []
|
|
59
91
|
|
|
60
92
|
|
|
61
93
|
class TranscriptDeltaDeliveryService:
|
|
@@ -153,6 +185,82 @@ class TranscriptDeltaDeliveryService:
|
|
|
153
185
|
self._last_error = ""
|
|
154
186
|
return True
|
|
155
187
|
|
|
188
|
+
def poll_tool_call_events(self) -> int:
|
|
189
|
+
settings = ToolCallEventSettings.from_config(self.ports.load_config())
|
|
190
|
+
if not settings.enabled:
|
|
191
|
+
return 0
|
|
192
|
+
path = self.ports.latest_transcript()
|
|
193
|
+
if path is None:
|
|
194
|
+
return 0
|
|
195
|
+
try:
|
|
196
|
+
path = path.resolve()
|
|
197
|
+
size = path.stat().st_size
|
|
198
|
+
except OSError:
|
|
199
|
+
return 0
|
|
200
|
+
scope = self.ports.scope()
|
|
201
|
+
runtime = str(scope.get("runtime") or "runtime")
|
|
202
|
+
session_id = str(scope.get("session_id") or path.stem)
|
|
203
|
+
source_key = hashlib.sha256(f"tool-call\0{path}".encode("utf-8")).hexdigest()
|
|
204
|
+
cursors = self._load_cursors()
|
|
205
|
+
sources = cursors.setdefault("tool_call_sources", {})
|
|
206
|
+
current = sources.get(source_key)
|
|
207
|
+
if not isinstance(current, dict):
|
|
208
|
+
offset = self._initial_tool_call_offset(settings, scope, path, size)
|
|
209
|
+
sources[source_key] = self._cursor_record(path, offset)
|
|
210
|
+
self._save_cursors(cursors)
|
|
211
|
+
if offset >= size:
|
|
212
|
+
return 0
|
|
213
|
+
current = sources[source_key]
|
|
214
|
+
offset = max(0, int(current.get("offset") or 0))
|
|
215
|
+
if size < offset:
|
|
216
|
+
offset = 0
|
|
217
|
+
current.pop("web_search_results", None)
|
|
218
|
+
payload = self._read_complete_batch(path, offset, settings.max_batch_bytes)
|
|
219
|
+
if not payload:
|
|
220
|
+
return 0
|
|
221
|
+
count = 0
|
|
222
|
+
search_state = current.setdefault("web_search_results", {})
|
|
223
|
+
for raw_line in payload.decode("utf-8", errors="replace").splitlines():
|
|
224
|
+
try:
|
|
225
|
+
record = json.loads(raw_line)
|
|
226
|
+
except (TypeError, ValueError):
|
|
227
|
+
continue
|
|
228
|
+
if not isinstance(record, dict):
|
|
229
|
+
continue
|
|
230
|
+
for result in project_web_search_results(record, runtime, search_state):
|
|
231
|
+
self.ports.event_publish(
|
|
232
|
+
level="info", category="tool.call", message=f"{runtime} web search result URLs",
|
|
233
|
+
source="cli-transcript", session_id=session_id, provider=runtime,
|
|
234
|
+
data=result,
|
|
235
|
+
)
|
|
236
|
+
count += 1
|
|
237
|
+
for call in project_transcript_tool_calls(record, runtime):
|
|
238
|
+
call_id = str(call.get("call_id") or "")
|
|
239
|
+
if call_id and any(
|
|
240
|
+
str((event.get("data") or {}).get("call_id") or "") == call_id
|
|
241
|
+
and (event.get("data") or {}).get("phase") != "result"
|
|
242
|
+
for event in self.ports.event_recent(limit=200, category="tool.call")
|
|
243
|
+
):
|
|
244
|
+
continue
|
|
245
|
+
data = dict(call)
|
|
246
|
+
if not settings.include_arguments:
|
|
247
|
+
data.pop("arguments", None)
|
|
248
|
+
self.ports.event_publish(
|
|
249
|
+
level="info",
|
|
250
|
+
category="tool.call",
|
|
251
|
+
message=f"{runtime} tool call: {call['name']}",
|
|
252
|
+
source="cli-transcript",
|
|
253
|
+
session_id=session_id,
|
|
254
|
+
provider=runtime,
|
|
255
|
+
model=str(call.get("model") or ""),
|
|
256
|
+
data={**data, "transcript_name": path.name},
|
|
257
|
+
)
|
|
258
|
+
count += 1
|
|
259
|
+
sources[source_key] = self._cursor_record(path, offset + len(payload))
|
|
260
|
+
sources[source_key]["web_search_results"] = search_state
|
|
261
|
+
self._save_cursors(cursors)
|
|
262
|
+
return count
|
|
263
|
+
|
|
156
264
|
@staticmethod
|
|
157
265
|
def _initial_offset(
|
|
158
266
|
settings: TranscriptDeliverySettings, scope: dict[str, Any], path: Path
|
|
@@ -175,7 +283,10 @@ class TranscriptDeltaDeliveryService:
|
|
|
175
283
|
try:
|
|
176
284
|
config = self.ports.load_config()
|
|
177
285
|
settings = TranscriptDeliverySettings.from_config(config)
|
|
178
|
-
|
|
286
|
+
tool_settings = ToolCallEventSettings.from_config(config)
|
|
287
|
+
interval = min(settings.poll_interval_seconds, tool_settings.poll_interval_seconds)
|
|
288
|
+
if tool_settings.enabled:
|
|
289
|
+
self.poll_tool_call_events()
|
|
179
290
|
if settings.enabled and settings.url:
|
|
180
291
|
self.poll_once()
|
|
181
292
|
except Exception as exc:
|
|
@@ -191,9 +302,36 @@ class TranscriptDeltaDeliveryService:
|
|
|
191
302
|
value = {}
|
|
192
303
|
if not isinstance(value.get("destinations"), dict):
|
|
193
304
|
value["destinations"] = {}
|
|
305
|
+
if not isinstance(value.get("tool_call_sources"), dict):
|
|
306
|
+
value["tool_call_sources"] = {}
|
|
194
307
|
value["version"] = 1
|
|
195
308
|
return value
|
|
196
309
|
|
|
310
|
+
@staticmethod
|
|
311
|
+
def _initial_tool_call_offset(
|
|
312
|
+
settings: ToolCallEventSettings,
|
|
313
|
+
scope: dict[str, Any],
|
|
314
|
+
path: Path,
|
|
315
|
+
size: int,
|
|
316
|
+
) -> int:
|
|
317
|
+
if settings.start_mode == "beginning":
|
|
318
|
+
return 0
|
|
319
|
+
boundary_path = scope.get("turn_scan_path")
|
|
320
|
+
if boundary_path is not None:
|
|
321
|
+
try:
|
|
322
|
+
if Path(boundary_path).resolve() == path:
|
|
323
|
+
return max(0, int(scope.get("turn_scan_offset") or 0))
|
|
324
|
+
except (OSError, TypeError, ValueError):
|
|
325
|
+
pass
|
|
326
|
+
started_at = float(scope.get("started_at") or 0)
|
|
327
|
+
if started_at > 0:
|
|
328
|
+
try:
|
|
329
|
+
if path.stat().st_mtime >= started_at - 1.0:
|
|
330
|
+
return 0
|
|
331
|
+
except OSError:
|
|
332
|
+
pass
|
|
333
|
+
return size
|
|
334
|
+
|
|
197
335
|
def _save_cursors(self, cursors: dict[str, Any]) -> None:
|
|
198
336
|
self.cursor_path.parent.mkdir(parents=True, exist_ok=True)
|
|
199
337
|
temporary = self.cursor_path.with_name(
|
|
@@ -330,4 +468,5 @@ __all__ = [
|
|
|
330
468
|
"TranscriptDeliveryPorts",
|
|
331
469
|
"TranscriptDeliverySettings",
|
|
332
470
|
"TranscriptDeltaDeliveryService",
|
|
471
|
+
"ToolCallEventSettings",
|
|
333
472
|
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Extract response URLs, never search arguments or result prose, from CLI records."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_SEARCH_NAMES = {"WebSearch", "web_search", "web.run", "web__run", "functions.web__run"}
|
|
12
|
+
_URL = re.compile(r"https?://[^\s<>\"\x00-\x1f]+")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_search_call(value: dict[str, Any]) -> bool:
|
|
16
|
+
name = value.get("name")
|
|
17
|
+
if name not in _SEARCH_NAMES:
|
|
18
|
+
return False
|
|
19
|
+
if name in {"WebSearch", "web_search"}:
|
|
20
|
+
return True
|
|
21
|
+
arguments = value.get("arguments", value.get("input"))
|
|
22
|
+
if isinstance(arguments, str):
|
|
23
|
+
try:
|
|
24
|
+
arguments = json.loads(arguments)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return False
|
|
27
|
+
return isinstance(arguments, dict) and bool(arguments.get("search_query"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _valid_url(value: Any) -> bool:
|
|
31
|
+
if not isinstance(value, str) or any(c.isspace() or ord(c) < 32 for c in value):
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
parsed = urlsplit(value)
|
|
35
|
+
return parsed.scheme in {"http", "https"} and bool(parsed.hostname) and not parsed.username and not parsed.password
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _urls(value: Any, *, text: bool = False) -> list[str]:
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
return [url for item in value for url in _urls(item, text=text)]
|
|
43
|
+
if isinstance(value, dict):
|
|
44
|
+
result = [value["url"]] if _valid_url(value.get("url")) else []
|
|
45
|
+
for key in ("sources", "results", "content", "annotations", "url_citation"):
|
|
46
|
+
result.extend(_urls(value.get(key), text=text))
|
|
47
|
+
if text and value.get("type") == "text":
|
|
48
|
+
result.extend(_urls(value.get("text"), text=True))
|
|
49
|
+
return result
|
|
50
|
+
if text and isinstance(value, str):
|
|
51
|
+
# Claude Code supplies a JSON Links list embedded in a text result.
|
|
52
|
+
match = re.search(r"(?:^|\n)Links:\s*", value)
|
|
53
|
+
if match:
|
|
54
|
+
try:
|
|
55
|
+
links, _ = json.JSONDecoder().raw_decode(value[match.end():])
|
|
56
|
+
return _urls(links)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return []
|
|
59
|
+
try:
|
|
60
|
+
decoded = json.loads(value)
|
|
61
|
+
except ValueError:
|
|
62
|
+
decoded = None
|
|
63
|
+
if isinstance(decoded, (dict, list)):
|
|
64
|
+
return _urls(decoded, text=True)
|
|
65
|
+
result = []
|
|
66
|
+
for match in _URL.finditer(value):
|
|
67
|
+
url = match.group().rstrip(".,;!?'\"]}")
|
|
68
|
+
while url.endswith(")") and url.count(")") > url.count("("):
|
|
69
|
+
url = url[:-1]
|
|
70
|
+
if _valid_url(url):
|
|
71
|
+
result.append(url)
|
|
72
|
+
return result
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def project_web_search_results(record: dict[str, Any], runtime: str, state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
77
|
+
"""State is persisted with the transcript offset, bounded to 512 calls/results."""
|
|
78
|
+
pending = state.setdefault("pending", {})
|
|
79
|
+
seen = state.setdefault("seen", [])
|
|
80
|
+
events = []
|
|
81
|
+
|
|
82
|
+
def emit(call_id: str, name: str, urls: list[str]) -> None:
|
|
83
|
+
urls = list(dict.fromkeys(urls))
|
|
84
|
+
if not urls:
|
|
85
|
+
return
|
|
86
|
+
digest = hashlib.sha256(json.dumps([call_id, urls], ensure_ascii=False).encode()).hexdigest()
|
|
87
|
+
if digest in seen:
|
|
88
|
+
return
|
|
89
|
+
seen.append(digest)
|
|
90
|
+
events.append({"call_id": call_id, "name": name, "runtime": runtime, "phase": "result", "urls": urls})
|
|
91
|
+
|
|
92
|
+
message = record.get("message") or {}
|
|
93
|
+
blocks = message.get("content", []) if isinstance(message, dict) else []
|
|
94
|
+
if isinstance(blocks, list):
|
|
95
|
+
for block in blocks:
|
|
96
|
+
if not isinstance(block, dict):
|
|
97
|
+
continue
|
|
98
|
+
kind = block.get("type")
|
|
99
|
+
if kind in {"tool_use", "server_tool_use"} and (record.get("type") == "assistant" or message.get("role") == "assistant") and _is_search_call(block):
|
|
100
|
+
if block.get("id"):
|
|
101
|
+
pending[str(block["id"])] = str(block["name"])
|
|
102
|
+
elif kind in {"tool_result", "web_search_tool_result"} and not block.get("is_error"):
|
|
103
|
+
call_id = str(block.get("tool_use_id") or "")
|
|
104
|
+
name = pending.get(call_id)
|
|
105
|
+
if name or kind == "web_search_tool_result":
|
|
106
|
+
emit(call_id, name or "web_search", _urls(block.get("content"), text=True))
|
|
107
|
+
|
|
108
|
+
payload = record.get("payload") or {}
|
|
109
|
+
if record.get("type") == "response_item" and isinstance(payload, dict):
|
|
110
|
+
kind = payload.get("type")
|
|
111
|
+
call_id = str(payload.get("call_id") or payload.get("id") or "")
|
|
112
|
+
if kind in {"function_call", "custom_tool_call"} and _is_search_call(payload) and call_id:
|
|
113
|
+
pending[call_id] = str(payload["name"])
|
|
114
|
+
elif kind in {"function_call_output", "custom_tool_call_output"} and call_id in pending:
|
|
115
|
+
emit(call_id, pending[call_id], _urls(payload.get("output"), text=True))
|
|
116
|
+
elif kind == "web_search_call" and payload.get("status") == "completed":
|
|
117
|
+
# Do not treat action.query or open_page input URLs as search results.
|
|
118
|
+
action = payload.get("action") or {}
|
|
119
|
+
sources = action.get("sources") if isinstance(action, dict) else None
|
|
120
|
+
emit(call_id, "web_search", _urls([sources, payload.get("results")]))
|
|
121
|
+
elif kind == "message" and payload.get("role") == "assistant":
|
|
122
|
+
# API-backed transcripts can retain structured URL citations.
|
|
123
|
+
for block in payload.get("content") or []:
|
|
124
|
+
if isinstance(block, dict):
|
|
125
|
+
annotations = [a for a in block.get("annotations", []) if isinstance(a, dict) and a.get("type") == "url_citation"]
|
|
126
|
+
emit(call_id, "web_search", _urls(annotations))
|
|
127
|
+
while len(pending) > 512:
|
|
128
|
+
del pending[next(iter(pending))]
|
|
129
|
+
del seen[:-512]
|
|
130
|
+
return events
|
|
@@ -29,6 +29,10 @@ _CLAUDE_COLLAPSED_PASTE_PATTERN = re.compile(
|
|
|
29
29
|
rb"\[Pasted(?:\x1b\[[0-?]*[ -/]*[@-~]|\s)+"
|
|
30
30
|
rb"text(?:\x1b\[[0-?]*[ -/]*[@-~]|\s)+#"
|
|
31
31
|
)
|
|
32
|
+
_VT_OSC_PATTERN = re.compile(rb"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
|
|
33
|
+
_VT_CURSOR_RIGHT_PATTERN = re.compile(rb"\x1b\[([0-9]*)(?:C|a)")
|
|
34
|
+
_VT_CSI_PATTERN = re.compile(rb"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
35
|
+
_VT_SINGLE_ESCAPE_PATTERN = re.compile(rb"\x1b[@-_]")
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
def _collapsed_paste_marker_count(output: bytes | str) -> int:
|
|
@@ -38,6 +42,21 @@ def _collapsed_paste_marker_count(output: bytes | str) -> int:
|
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
|
|
45
|
+
def _visible_terminal_text(output: bytes | str) -> str:
|
|
46
|
+
"""Project ConPTY output to comparable visible text without mutating input."""
|
|
47
|
+
|
|
48
|
+
data = output.encode("utf-8", errors="replace") if isinstance(output, str) else bytes(output)
|
|
49
|
+
data = _VT_OSC_PATTERN.sub(b"", data)
|
|
50
|
+
data = _VT_CURSOR_RIGHT_PATTERN.sub(
|
|
51
|
+
lambda match: b" " * max(1, min(4096, int(match.group(1) or b"1"))),
|
|
52
|
+
data,
|
|
53
|
+
)
|
|
54
|
+
data = _VT_CSI_PATTERN.sub(b"", data)
|
|
55
|
+
data = _VT_SINGLE_ESCAPE_PATTERN.sub(b"", data)
|
|
56
|
+
text = data.decode("utf-8", errors="replace")
|
|
57
|
+
return " ".join(text.replace("\x00", "").split())
|
|
58
|
+
|
|
59
|
+
|
|
41
60
|
def conpty_enabled(
|
|
42
61
|
environment: Mapping[str, str] | None = None,
|
|
43
62
|
*,
|
|
@@ -202,8 +221,9 @@ class WindowsConPtySession:
|
|
|
202
221
|
prompt = WindowsConPtySession.normalize_prompt(str(expected_prompt or ""))
|
|
203
222
|
if not prompt:
|
|
204
223
|
return bool(output)
|
|
205
|
-
prefix = prompt[:48]
|
|
206
|
-
|
|
224
|
+
prefix = _visible_terminal_text(prompt)[:48]
|
|
225
|
+
visible = _visible_terminal_text(output)
|
|
226
|
+
return bool(prefix and prefix in visible) or _collapsed_paste_marker_count(output) > 0
|
|
207
227
|
|
|
208
228
|
@staticmethod
|
|
209
229
|
def _prompt_rendered_since(
|
|
@@ -214,13 +234,44 @@ class WindowsConPtySession:
|
|
|
214
234
|
if current == baseline:
|
|
215
235
|
return False
|
|
216
236
|
prompt = WindowsConPtySession.normalize_prompt(str(expected_prompt or ""))
|
|
217
|
-
prefix = prompt[:48]
|
|
218
|
-
|
|
237
|
+
prefix = _visible_terminal_text(prompt)[:48]
|
|
238
|
+
current_visible = _visible_terminal_text(current)
|
|
239
|
+
baseline_visible = _visible_terminal_text(baseline)
|
|
240
|
+
if prefix and current_visible.count(prefix) > baseline_visible.count(prefix):
|
|
219
241
|
return True
|
|
220
242
|
if prompt and _collapsed_paste_marker_count(current) > _collapsed_paste_marker_count(baseline):
|
|
221
243
|
return True
|
|
222
244
|
return not prompt
|
|
223
245
|
|
|
246
|
+
def clear_unsubmitted_prompt(
|
|
247
|
+
self,
|
|
248
|
+
clear_input: bytes,
|
|
249
|
+
expected_prompt: str,
|
|
250
|
+
timeout_seconds: float = 2.0,
|
|
251
|
+
) -> bool:
|
|
252
|
+
"""Clear a draft and require a settled child redraw before it is retry-safe."""
|
|
253
|
+
|
|
254
|
+
checkpoint = self.prompt_readiness_checkpoint()
|
|
255
|
+
self.write(clear_input)
|
|
256
|
+
cursor = checkpoint
|
|
257
|
+
observed = bytearray()
|
|
258
|
+
stable_since: float | None = None
|
|
259
|
+
deadline = time.monotonic() + max(0.0, float(timeout_seconds))
|
|
260
|
+
prefix = _visible_terminal_text(self.normalize_prompt(expected_prompt))[:48]
|
|
261
|
+
while True:
|
|
262
|
+
now = time.monotonic()
|
|
263
|
+
chunk, cursor = self._output_since(cursor)
|
|
264
|
+
if chunk:
|
|
265
|
+
observed.extend(chunk)
|
|
266
|
+
del observed[: max(0, len(observed) - 64 * 1024)]
|
|
267
|
+
stable_since = now
|
|
268
|
+
if observed and stable_since is not None and now - stable_since >= 0.25:
|
|
269
|
+
visible = _visible_terminal_text(bytes(observed))
|
|
270
|
+
return bool(visible) and (not prefix or prefix not in visible)
|
|
271
|
+
if now >= deadline:
|
|
272
|
+
return False
|
|
273
|
+
time.sleep(0.02)
|
|
274
|
+
|
|
224
275
|
@staticmethod
|
|
225
276
|
def pending_input_events() -> None:
|
|
226
277
|
return None
|
|
@@ -320,16 +371,49 @@ class WindowsConPtySession:
|
|
|
320
371
|
def kill(self) -> None:
|
|
321
372
|
self.terminate()
|
|
322
373
|
|
|
323
|
-
def resize_if_needed(self) ->
|
|
374
|
+
def resize_if_needed(self) -> bool:
|
|
324
375
|
if not self._hpc or not self._kernel32:
|
|
325
|
-
return
|
|
376
|
+
return False
|
|
326
377
|
size = self._terminal_size()
|
|
327
378
|
if size == self._last_size:
|
|
328
|
-
return
|
|
379
|
+
return False
|
|
380
|
+
checkpoint = self.prompt_readiness_checkpoint()
|
|
329
381
|
coord = self._coord_type(size[0], size[1])
|
|
330
|
-
|
|
382
|
+
try:
|
|
383
|
+
result = int(self._kernel32.ResizePseudoConsole(self._hpc, coord))
|
|
384
|
+
except Exception as exc:
|
|
385
|
+
self._log(
|
|
386
|
+
"ERROR",
|
|
387
|
+
"channel_windows_conpty_resize_failed "
|
|
388
|
+
f"cols={size[0]} rows={size[1]} error={type(exc).__name__}: {exc}",
|
|
389
|
+
)
|
|
390
|
+
return False
|
|
331
391
|
if result == 0:
|
|
332
392
|
self._last_size = size
|
|
393
|
+
redrawn = self._wait_for_output_change(checkpoint, 0.75)
|
|
394
|
+
self._log(
|
|
395
|
+
"INFO" if redrawn else "WARN",
|
|
396
|
+
"channel_windows_conpty_resize "
|
|
397
|
+
f"cols={size[0]} rows={size[1]} "
|
|
398
|
+
f"redraw={'observed' if redrawn else 'timeout'}",
|
|
399
|
+
)
|
|
400
|
+
return True
|
|
401
|
+
self._log(
|
|
402
|
+
"ERROR",
|
|
403
|
+
"channel_windows_conpty_resize_failed "
|
|
404
|
+
f"cols={size[0]} rows={size[1]} hresult=0x{result & 0xFFFFFFFF:08X}",
|
|
405
|
+
)
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
def _wait_for_output_change(self, checkpoint: int, timeout_seconds: float) -> bool:
|
|
409
|
+
deadline = time.monotonic() + max(0.0, float(timeout_seconds))
|
|
410
|
+
while True:
|
|
411
|
+
chunk, _cursor = self._output_since(checkpoint)
|
|
412
|
+
if chunk:
|
|
413
|
+
return True
|
|
414
|
+
if time.monotonic() >= deadline:
|
|
415
|
+
return False
|
|
416
|
+
time.sleep(0.02)
|
|
333
417
|
|
|
334
418
|
def close(self) -> None:
|
|
335
419
|
if self._closed:
|
|
@@ -14,6 +14,8 @@ import time
|
|
|
14
14
|
import uuid
|
|
15
15
|
from typing import Any, Callable
|
|
16
16
|
|
|
17
|
+
from .managed_tool_injection import select_managed_tools
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
_SERVER_ID = re.compile(r"^[A-Za-z0-9_-]{1,80}$")
|
|
19
21
|
_RUNTIMES = frozenset({"claude", "codex", "codex-app-server"})
|
|
@@ -247,13 +249,14 @@ class WorkspaceMcpLaunchService:
|
|
|
247
249
|
config: dict[str, Any],
|
|
248
250
|
environment: Mapping[str, str] | None = None,
|
|
249
251
|
injected_servers: Mapping[str, Any] | None = None,
|
|
252
|
+
*, native: bool = False,
|
|
250
253
|
) -> WorkspaceMcpLaunch:
|
|
251
254
|
if runtime not in _RUNTIMES:
|
|
252
255
|
raise ValueError(f"Unsupported workspace MCP runtime: {runtime}")
|
|
253
256
|
self.recover_stale()
|
|
254
257
|
servers = workspace_mcp_servers(config, runtime)
|
|
255
258
|
if injected_servers:
|
|
256
|
-
injected = {"workspace_mcp": {"servers":
|
|
259
|
+
injected = {"workspace_mcp": {"servers": select_managed_tools(injected_servers, native=native)}}
|
|
257
260
|
servers.update(workspace_mcp_servers(injected, runtime))
|
|
258
261
|
if not servers:
|
|
259
262
|
return WorkspaceMcpLaunch()
|
package/docs/Configuration.md
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
| `model-registry.json` | 모델 레지스트리 |
|
|
36
36
|
| `ollama-model-catalog.json` | Ollama 모델 카탈로그 캐시 (TTL: 24시간) |
|
|
37
37
|
| `chat-messages.jsonl` | 채팅 메시지 (최대 20MB) |
|
|
38
|
+
| `workspaces/<workspace-id>/runtime-inputs.jsonl` | TUI/세션 소켓으로 전달할 private 입력 큐 |
|
|
39
|
+
| `workspaces/<workspace-id>/runtime-input-status.jsonl` | private 입력의 queued/submitted/replied/failed 상태 이력 |
|
|
38
40
|
| `channel-probe-cache.json` | 채널 프로브 캐시 |
|
|
39
41
|
| `launch-state.json` | 실행 상태 |
|
|
40
42
|
| `tts-reference-audio/*.bin` | 업로드한 TTS reference의 private binary sidecar (`0600` 시도) |
|
|
@@ -235,6 +237,13 @@ Alibaba Model Studio Token Plan(`alitoken`)의 native Responses endpoint는 실
|
|
|
235
237
|
10 MiB를 넘는 단일 비축약 항목은 upstream으로 보내거나 재시도하지 않고 로컬 413으로 종료한다.
|
|
236
238
|
`alitoken-individual`과 다른 provider에는 이 wire 정책을 적용하지 않는다.
|
|
237
239
|
|
|
240
|
+
Alibaba의 Responses session cache는 요청 header가 없으면 기본적으로 비활성화된다.
|
|
241
|
+
`alims-intl`과 `alitoken`의 `openai_responses` 요청에는
|
|
242
|
+
`x-dashscope-session-cache: enable`을 기본 전송한다. 이 값은 provider의
|
|
243
|
+
`protocol_headers.openai_responses` 설정으로 관리되며 Chat/Anthropic wire에는 복사하지
|
|
244
|
+
않는다. 이 범용 protocol header 설정은 `authorization`, API key, cookie, host,
|
|
245
|
+
content-length 및 hop-by-hop header를 덮어쓸 수 없다.
|
|
246
|
+
|
|
238
247
|
---
|
|
239
248
|
|
|
240
249
|
## 원격 시스템 지침
|
|
@@ -393,20 +402,27 @@ VT 활성화가 실패한 classic/legacy console에는 escape 문자열을 raw t
|
|
|
393
402
|
연기한다. 따라서 wake 주입 앞의 `Ctrl+U`가 사용자가 먼저 입력한 글자를 지우지
|
|
394
403
|
않으며, 사용자가 Enter로 draft를 제출한 뒤 기존 polling 절차가 다시 진행된다.
|
|
395
404
|
|
|
405
|
+
ConPTY prompt-render detector는 캡처된 UTF-8 출력을 기준으로 ANSI CSI/OSC/SGR을
|
|
406
|
+
가시 텍스트로 투영한 뒤 원문 prefix를 비교한다. 따라서 한글 바이트와 ANSI 색상·
|
|
407
|
+
cursor 이동 시퀀스가 섞여도 화면에 표시된 원문을 감지한다. 이 투영은 감지에만
|
|
408
|
+
사용하며 실제 입력 원문에는 표식이나 다른 문자를 추가하지 않는다.
|
|
409
|
+
|
|
396
410
|
ConPTY 생성이 불가능하거나 `CIEL_RUNTIME_WINDOWS_CONPTY=0`으로 명시적으로 끈
|
|
397
411
|
경우에만 기존 Windows Console 입력 큐 호환 경로를 사용한다. 이 호환 경로에서는
|
|
398
412
|
`clear → body → submit`을 각각 큐가 소비한 뒤 진행하고, 여러 줄의 외부 메시지는
|
|
399
413
|
줄바꿈이 Enter 키로 해석되지 않도록 한 줄로 정규화한다. 완전한 wake prompt 전달
|
|
400
|
-
|
|
401
|
-
|
|
414
|
+
확인에 실패하면 `Ctrl+U`로 해당 draft 제거를 한 번 요청하고 ConPTY redraw로 제거를
|
|
415
|
+
확인한다. 제거 여부와 관계없이 같은 원문을 자동 재입력하지 않으며 요청 상태를
|
|
416
|
+
`failed`로 유지한다. 앞선 실패 요청은 다음 입력을 차단한다. ASCII sentinel을
|
|
417
|
+
추가하거나 router request-body 전송으로 자동 변경하지 않는다.
|
|
402
418
|
|
|
403
419
|
호환 경로는 실행 중 Win32 `ENABLE_MOUSE_INPUT` bit를 끄고 종료할 때 원래 입력
|
|
404
420
|
mode를 복원한다. `CIEL_RUNTIME_WINDOWS_CONSOLE_MOUSE_FILTER=0`은 이 Win32 guard를,
|
|
405
421
|
`CIEL_RUNTIME_TERMINAL_INPUT_MODE_RESET=0`은 안전한 VT mode reset을 각각 끈다.
|
|
406
422
|
정확한 `ESC[20~`는 Microsoft VT 입력 표의 F9이므로 mouse report로 제거하지 않는다.
|
|
407
423
|
|
|
408
|
-
|
|
409
|
-
|
|
424
|
+
ConPTY resize 실패는 요청 크기와 HRESULT/예외를 router 로그에 기록한다. 성공한
|
|
425
|
+
resize 뒤에는 child output 발생 여부를 확인해 `redraw=observed|timeout`을 남긴다.
|
|
410
426
|
|
|
411
427
|
---
|
|
412
428
|
|