@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/ciel_runtime.py +2553 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -0,0 +1,332 @@
1
+ """Channel tool-context and durable LLM delivery cursor bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from http.server import BaseHTTPRequestHandler
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Protocol
9
+
10
+ from .channel_cursor_repository import (
11
+ ChannelCursorRepository,
12
+ ChannelCursorStatePolicy,
13
+ )
14
+ from .channel_cursor_service import (
15
+ ChannelDeliveryCursorCommitter,
16
+ ChannelDeliveryCursorPorts,
17
+ )
18
+ from .channel_tool_context import (
19
+ ChannelToolContextPolicy,
20
+ ChannelToolContextPorts,
21
+ ChannelToolContextRepository,
22
+ ChannelToolContextService,
23
+ )
24
+
25
+
26
+ class LockPort(Protocol):
27
+ def __enter__(self) -> object: ...
28
+
29
+ def __exit__(
30
+ self,
31
+ exc_type: type[BaseException] | None,
32
+ exc_value: BaseException | None,
33
+ traceback: object | None,
34
+ ) -> None: ...
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ChannelToolContextFactoryPorts:
39
+ contexts: dict[str, dict[str, Any]]
40
+ lock: LockPort
41
+ context_limit: int
42
+ max_inject: int
43
+ prompt_limit: int
44
+ content_to_text: Callable[[Any], str]
45
+ truncate: Callable[..., str]
46
+ now: Callable[[], float]
47
+ log: Callable[[str, str], None]
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class ChannelLlmCursorPorts:
52
+ repository: Callable[[Path], ChannelCursorRepository]
53
+ cursor_path: Path
54
+ clear_floor_path: Path
55
+ lock: LockPort
56
+ cache_read: Callable[[], int | None]
57
+ cache_write: Callable[[int | None], None]
58
+ scan_max_id: Callable[[], int]
59
+ now: Callable[[], float]
60
+ log: Callable[[str, str], None]
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class ChannelLaunchCursorPorts:
65
+ recent_seconds: Callable[[], float]
66
+ scan_max_before_epoch: Callable[[float], int]
67
+ write_launch_guard: Callable[[int], None]
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class ChannelDeliveryCommitPorts:
72
+ response_status: Callable[[Any], int | None]
73
+ metadata_enabled: Callable[[dict[str, Any] | None], bool]
74
+ delivery_confirmed: Callable[[Any], bool]
75
+ read_cursor: Callable[[], int]
76
+ write_cursor: Callable[[int], None]
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class ChannelDeliveryContext:
81
+ tools: ChannelToolContextFactoryPorts
82
+ cursor: ChannelLlmCursorPorts
83
+ launch: ChannelLaunchCursorPorts
84
+ commit: ChannelDeliveryCommitPorts
85
+
86
+ def tool_context_service(self) -> ChannelToolContextService:
87
+ ports = self.tools
88
+ return ChannelToolContextService(
89
+ repository=ChannelToolContextRepository(
90
+ contexts=ports.contexts,
91
+ lock=ports.lock,
92
+ limit=ports.context_limit,
93
+ ),
94
+ policy=ChannelToolContextPolicy(
95
+ max_inject=ports.max_inject,
96
+ prompt_limit=ports.prompt_limit,
97
+ ),
98
+ ports=ChannelToolContextPorts(
99
+ content_to_text=ports.content_to_text,
100
+ truncate=ports.truncate,
101
+ now=ports.now,
102
+ log=ports.log,
103
+ ),
104
+ )
105
+
106
+ def injected_prompt_text(self, body: dict[str, Any]) -> str:
107
+ return self.tool_context_service().prompt_text(body)
108
+
109
+ def remember_injected_tool_use(
110
+ self,
111
+ source_body: dict[str, Any] | None,
112
+ tool_use_id: str,
113
+ tool_name: str,
114
+ tool_input: Any,
115
+ ) -> None:
116
+ self.tool_context_service().remember(
117
+ source_body, tool_use_id, tool_name, tool_input
118
+ )
119
+
120
+ def remember_injected_tool_uses(
121
+ self, source_body: dict[str, Any] | None, message: dict[str, Any]
122
+ ) -> None:
123
+ self.tool_context_service().remember_message(source_body, message)
124
+
125
+ def take_tool_result_contexts(
126
+ self, body: dict[str, Any]
127
+ ) -> list[tuple[str, dict[str, Any]]]:
128
+ return self.tool_context_service().repository.take_for_body(
129
+ body, self.tools.max_inject
130
+ )
131
+
132
+ def body_with_tool_result_context(
133
+ self, body: dict[str, Any]
134
+ ) -> dict[str, Any]:
135
+ return self.tool_context_service().inject_followup(body)
136
+
137
+ def write_cursor(self, last_id: int) -> None:
138
+ self.cursor.repository(self.cursor.cursor_path).write(last_id)
139
+
140
+ def read_cursor(self) -> int:
141
+ resolution = ChannelCursorStatePolicy.resolve_read(
142
+ self.cursor.repository(self.cursor.cursor_path).read(),
143
+ self.cursor.cache_read(),
144
+ self.cursor.scan_max_id,
145
+ )
146
+ self.cursor.cache_write(resolution.value)
147
+ if resolution.rolled_back:
148
+ self.cursor.log(
149
+ "WARN",
150
+ "channel_llm_cursor_queue_generation_reset "
151
+ f"recovered_cursor={resolution.value}",
152
+ )
153
+ if resolution.persist:
154
+ self.write_cursor(resolution.value)
155
+ return resolution.value
156
+
157
+ def read_clear_floor(self) -> int:
158
+ return self.cursor.repository(self.cursor.clear_floor_path).read() or 0
159
+
160
+ def write_clear_floor(self, last_id: int) -> None:
161
+ self.cursor.repository(self.cursor.clear_floor_path).write(
162
+ last_id, metadata={"updated_at": self.cursor.now()}
163
+ )
164
+
165
+ def clamp_to_clear_floor(self, recovered: int) -> int:
166
+ clear_floor = self.read_clear_floor()
167
+ if clear_floor > 0 and recovered < clear_floor:
168
+ self.cursor.log(
169
+ "INFO",
170
+ "channel_stdin_proxy_recovery_clamped "
171
+ f"recovered_cursor={recovered} clear_floor={clear_floor}",
172
+ )
173
+ return clear_floor
174
+ return recovered
175
+
176
+ def reset_cursor(self, last_id: int | None = None) -> int:
177
+ with self.cursor.lock:
178
+ target = max(
179
+ 0,
180
+ int(
181
+ last_id
182
+ if last_id is not None
183
+ else self.cursor.scan_max_id()
184
+ ),
185
+ )
186
+ self.cursor.cache_write(target)
187
+ self.write_cursor(target)
188
+ return target
189
+
190
+ def ensure_cursor_initialized(self) -> int:
191
+ with self.cursor.lock:
192
+ return self.read_cursor()
193
+
194
+ def prepare_for_launch(self) -> int:
195
+ current = self.ensure_cursor_initialized()
196
+ recent_seconds = self.launch.recent_seconds()
197
+ if recent_seconds <= 0:
198
+ target = self.cursor.scan_max_id()
199
+ else:
200
+ target = self.launch.scan_max_before_epoch(
201
+ self.cursor.now() - recent_seconds
202
+ )
203
+ # The stale cutoff is a durable safety boundary, not merely a startup
204
+ # cursor optimization. Transcript recovery may legitimately rewind a
205
+ # cursor for a recently queued command that was not completed before a
206
+ # crash, but it must never reopen commands older than this launch's
207
+ # replay window.
208
+ self.write_clear_floor(max(self.read_clear_floor(), target))
209
+ last_id = self.reset_cursor(max(current, target))
210
+ self.launch.write_launch_guard(last_id)
211
+ self.cursor.log(
212
+ "INFO",
213
+ "channel_llm_cursor_fast_forward_on_launch "
214
+ f"last_id={last_id} previous_cursor={current} "
215
+ f"recent_seconds={recent_seconds:g}",
216
+ )
217
+ return last_id
218
+
219
+ def commit_if_newer(self, last_id: int | None) -> None:
220
+ with self.cursor.lock:
221
+ current = self.commit.read_cursor()
222
+ target = ChannelCursorStatePolicy.newer(last_id, current)
223
+ if target is None:
224
+ return
225
+ self.cursor.cache_write(target)
226
+ try:
227
+ self.commit.write_cursor(target)
228
+ except Exception as exc:
229
+ self.cursor.log(
230
+ "WARN",
231
+ "channel_llm_cursor_write_failed "
232
+ f"error={type(exc).__name__}: {exc}",
233
+ )
234
+
235
+ def commit_pending(
236
+ self,
237
+ body: dict[str, Any],
238
+ handler: BaseHTTPRequestHandler | None = None,
239
+ metadata: dict[str, Any] | None = None,
240
+ ) -> None:
241
+ ChannelDeliveryCursorCommitter(
242
+ ChannelDeliveryCursorPorts(
243
+ response_status=self.commit.response_status,
244
+ metadata_enabled=self.commit.metadata_enabled,
245
+ delivery_confirmed=self.commit.delivery_confirmed,
246
+ commit_if_newer=self.commit_if_newer,
247
+ log=self.cursor.log,
248
+ )
249
+ ).commit(body, handler, metadata)
250
+
251
+
252
+ @dataclass(frozen=True, slots=True)
253
+ class ChannelDeliveryCompatibilityApi:
254
+ context: Callable[[], ChannelDeliveryContext]
255
+
256
+ def tool_context_service(self) -> ChannelToolContextService:
257
+ return self.context().tool_context_service()
258
+
259
+ def injected_prompt_text(self, body: dict[str, Any]) -> str:
260
+ return self.context().injected_prompt_text(body)
261
+
262
+ def remember_injected_tool_use(
263
+ self,
264
+ source_body: dict[str, Any] | None,
265
+ tool_use_id: str,
266
+ tool_name: str,
267
+ tool_input: Any,
268
+ ) -> None:
269
+ self.context().remember_injected_tool_use(
270
+ source_body, tool_use_id, tool_name, tool_input
271
+ )
272
+
273
+ def remember_injected_tool_uses(
274
+ self, source_body: dict[str, Any] | None, message: dict[str, Any]
275
+ ) -> None:
276
+ self.context().remember_injected_tool_uses(source_body, message)
277
+
278
+ def take_tool_result_contexts(
279
+ self, body: dict[str, Any]
280
+ ) -> list[tuple[str, dict[str, Any]]]:
281
+ return self.context().take_tool_result_contexts(body)
282
+
283
+ def body_with_tool_result_context(
284
+ self, body: dict[str, Any]
285
+ ) -> dict[str, Any]:
286
+ return self.context().body_with_tool_result_context(body)
287
+
288
+ def write_cursor(self, last_id: int) -> None:
289
+ self.context().write_cursor(last_id)
290
+
291
+ def read_cursor(self) -> int:
292
+ return self.context().read_cursor()
293
+
294
+ def read_clear_floor(self) -> int:
295
+ return self.context().read_clear_floor()
296
+
297
+ def write_clear_floor(self, last_id: int) -> None:
298
+ self.context().write_clear_floor(last_id)
299
+
300
+ def clamp_to_clear_floor(self, recovered: int) -> int:
301
+ return self.context().clamp_to_clear_floor(recovered)
302
+
303
+ def reset_cursor(self, last_id: int | None = None) -> int:
304
+ return self.context().reset_cursor(last_id)
305
+
306
+ def ensure_cursor_initialized(self) -> int:
307
+ return self.context().ensure_cursor_initialized()
308
+
309
+ def prepare_for_launch(self) -> int:
310
+ return self.context().prepare_for_launch()
311
+
312
+ def commit_if_newer(self, last_id: int | None) -> None:
313
+ self.context().commit_if_newer(last_id)
314
+
315
+ def commit_pending(
316
+ self,
317
+ body: dict[str, Any],
318
+ handler: BaseHTTPRequestHandler | None = None,
319
+ metadata: dict[str, Any] | None = None,
320
+ ) -> None:
321
+ self.context().commit_pending(body, handler, metadata)
322
+
323
+
324
+ __all__ = [
325
+ "ChannelDeliveryCommitPorts",
326
+ "ChannelDeliveryCompatibilityApi",
327
+ "ChannelDeliveryContext",
328
+ "ChannelLaunchCursorPorts",
329
+ "ChannelLlmCursorPorts",
330
+ "ChannelToolContextFactoryPorts",
331
+ "LockPort",
332
+ ]
@@ -0,0 +1,313 @@
1
+ """Built-in channel MCP server bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from http.server import BaseHTTPRequestHandler
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ from .channel_cursor_repository import ChannelCursorRepository
12
+ from .channel_cursor_service import (
13
+ ChannelCursorService,
14
+ ChannelCursorServices,
15
+ ChannelResumePolicy,
16
+ ChannelResumeServices,
17
+ )
18
+ from .channel_mcp_http_controller import (
19
+ ChannelMcpHttpController,
20
+ ChannelMcpRpcServices,
21
+ ChannelMcpSessionStore,
22
+ ChannelMcpStreamServices,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class ChannelMcpRuntimePorts:
28
+ version: str
29
+ process_id: Callable[[], int]
30
+ timestamp_ns: Callable[[], int]
31
+ condition: Any
32
+ log: Callable[[str, str], None]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ChannelMcpStatePorts:
37
+ sessions: dict[str, dict[str, Any]]
38
+ session_lock: Any
39
+ cursor_lock: Any
40
+ cursor_path: Path
41
+ cached_cursor: Callable[[], int | None]
42
+ cache_cursor: Callable[[int], None]
43
+ scan_tail: Callable[[], int]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class ChannelMcpProjectionPorts:
48
+ capabilities: Callable[[], dict[str, Any]]
49
+ notifications: Callable[..., list[tuple[int, dict[str, Any]]]]
50
+ read_messages: Callable[..., list[dict[str, Any]]]
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class ChannelMcpRpcPorts:
55
+ tool_schemas: Callable[[], list[dict[str, Any]]]
56
+ tool_call_response: Callable[[Any, dict[str, Any]], dict[str, Any]]
57
+ write_json: Callable[..., None]
58
+ write_accepted: Callable[..., None]
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class ChannelMcpResumePorts:
63
+ query_params: Callable[..., dict[str, list[str]]]
64
+ first_param: Callable[..., str]
65
+ ensure_cursor: Callable[[], int]
66
+ update_cursor: Callable[[int], None]
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class ChannelMcpContext:
71
+ runtime: ChannelMcpRuntimePorts
72
+ state: ChannelMcpStatePorts
73
+ projection: ChannelMcpProjectionPorts
74
+ rpc: ChannelMcpRpcPorts
75
+ resume: ChannelMcpResumePorts
76
+ cursor_repository: Callable[[Path], ChannelCursorRepository]
77
+
78
+ def new_session_id(self) -> str:
79
+ return f"s{self.runtime.process_id()}-{self.runtime.timestamp_ns()}"
80
+
81
+ @staticmethod
82
+ def write_sse_event(
83
+ handler: BaseHTTPRequestHandler,
84
+ event: str,
85
+ data: Any,
86
+ event_id: int | None = None,
87
+ ) -> None:
88
+ if event_id is not None:
89
+ handler.wfile.write(f"id: {event_id}\n".encode("utf-8"))
90
+ handler.wfile.write(f"event: {event}\n".encode("utf-8"))
91
+ payload = (
92
+ data
93
+ if isinstance(data, str)
94
+ else json.dumps(data, ensure_ascii=False, separators=(",", ":"))
95
+ )
96
+ for line in payload.splitlines() or [""]:
97
+ handler.wfile.write(f"data: {line}\n".encode("utf-8"))
98
+ handler.wfile.write(b"\n")
99
+ handler.wfile.flush()
100
+
101
+ @staticmethod
102
+ def send_sse_headers(handler: BaseHTTPRequestHandler) -> None:
103
+ handler.send_response(200)
104
+ handler.send_header("content-type", "text/event-stream")
105
+ handler.send_header("cache-control", "no-cache, no-transform")
106
+ handler.send_header("connection", "keep-alive")
107
+ handler.send_header("x-accel-buffering", "no")
108
+ handler.end_headers()
109
+
110
+ def enqueue(self, session: str, payload: dict[str, Any]) -> bool:
111
+ if not session:
112
+ return False
113
+ with self.state.session_lock:
114
+ state = self.state.sessions.get(session)
115
+ if not state:
116
+ return False
117
+ outbox = state.setdefault("outbox", [])
118
+ if isinstance(outbox, list):
119
+ outbox.append(payload)
120
+ else:
121
+ state["outbox"] = [payload]
122
+ with self.runtime.condition:
123
+ self.runtime.condition.notify_all()
124
+ return True
125
+
126
+ def take_outbox(self, session: str) -> list[dict[str, Any]]:
127
+ with self.state.session_lock:
128
+ state = self.state.sessions.get(session)
129
+ if not state:
130
+ return []
131
+ outbox = state.get("outbox")
132
+ if not isinstance(outbox, list) or not outbox:
133
+ return []
134
+ state["outbox"] = []
135
+ return [item for item in outbox if isinstance(item, dict)]
136
+
137
+ def initialize_response(self, request_id: Any, protocol: str) -> dict[str, Any]:
138
+ del protocol
139
+ return {
140
+ "jsonrpc": "2.0",
141
+ "id": request_id,
142
+ "result": {
143
+ "protocolVersion": "2024-11-05",
144
+ "capabilities": self.projection.capabilities(),
145
+ "serverInfo": {
146
+ "name": "ciel-runtime-router",
147
+ "version": self.runtime.version,
148
+ },
149
+ },
150
+ }
151
+
152
+ def cursor_service(self) -> ChannelCursorService:
153
+ return ChannelCursorService(
154
+ ChannelCursorServices(
155
+ repository=self.cursor_repository(self.state.cursor_path),
156
+ lock=self.state.cursor_lock,
157
+ cached=self.state.cached_cursor,
158
+ cache=self.state.cache_cursor,
159
+ scan_tail=self.state.scan_tail,
160
+ )
161
+ )
162
+
163
+ def write_cursor(self, last_id: int) -> None:
164
+ self.cursor_repository(self.state.cursor_path).write(last_id)
165
+
166
+ def read_cursor(self) -> int:
167
+ return self.cursor_service().read_locked()
168
+
169
+ def ensure_cursor(self) -> int:
170
+ return self.cursor_service().ensure_initialized()
171
+
172
+ def update_cursor(self, last_id: int) -> None:
173
+ self.cursor_service().update(last_id)
174
+
175
+ def resume_policy(self) -> ChannelResumePolicy:
176
+ return ChannelResumePolicy(
177
+ ChannelResumeServices(
178
+ query_params=self.resume.query_params,
179
+ first_param=self.resume.first_param,
180
+ ensure_cursor=self.resume.ensure_cursor,
181
+ update_cursor=self.resume.update_cursor,
182
+ log=self.runtime.log,
183
+ )
184
+ )
185
+
186
+ def client_last_event_id(
187
+ self, handler: BaseHTTPRequestHandler
188
+ ) -> int | None:
189
+ return self.resume_policy().client_last_event_id(handler)
190
+
191
+ def session_start_last_id(self, handler: BaseHTTPRequestHandler) -> int:
192
+ return self.resume_policy().session_start_last_id(handler)
193
+
194
+ def controller(self) -> ChannelMcpHttpController:
195
+ return ChannelMcpHttpController(
196
+ store=ChannelMcpSessionStore(
197
+ self.state.sessions, self.state.session_lock
198
+ ),
199
+ stream=ChannelMcpStreamServices(
200
+ new_session_id=self.new_session_id,
201
+ start_last_id=self.session_start_last_id,
202
+ send_headers=self.send_sse_headers,
203
+ write_event=self.write_sse_event,
204
+ take_outbox=self.take_outbox,
205
+ read_messages=self.projection.read_messages,
206
+ project_notifications=self.projection.notifications,
207
+ update_cursor=self.update_cursor,
208
+ condition=self.runtime.condition,
209
+ log=self.runtime.log,
210
+ ),
211
+ rpc=ChannelMcpRpcServices(
212
+ initialize_response=self.initialize_response,
213
+ tool_schemas=self.rpc.tool_schemas,
214
+ tool_call_response=self.rpc.tool_call_response,
215
+ enqueue=self.enqueue,
216
+ write_json=self.rpc.write_json,
217
+ write_accepted=self.rpc.write_accepted,
218
+ log=self.runtime.log,
219
+ ),
220
+ )
221
+
222
+ def get(self, handler: BaseHTTPRequestHandler, path: str) -> bool:
223
+ return self.controller().get(handler, path)
224
+
225
+ def post(
226
+ self,
227
+ handler: BaseHTTPRequestHandler,
228
+ path: str,
229
+ body: dict[str, Any],
230
+ ) -> bool:
231
+ return self.controller().post(handler, path, body)
232
+
233
+
234
+ @dataclass(frozen=True, slots=True)
235
+ class ChannelMcpCompatibilityApi:
236
+ context: Callable[[], ChannelMcpContext]
237
+
238
+ def new_session_id(self) -> str:
239
+ return self.context().new_session_id()
240
+
241
+ def write_sse_event(
242
+ self,
243
+ handler: BaseHTTPRequestHandler,
244
+ event: str,
245
+ data: Any,
246
+ event_id: int | None = None,
247
+ ) -> None:
248
+ self.context().write_sse_event(handler, event, data, event_id)
249
+
250
+ def send_sse_headers(self, handler: BaseHTTPRequestHandler) -> None:
251
+ self.context().send_sse_headers(handler)
252
+
253
+ def enqueue(self, session: str, payload: dict[str, Any]) -> bool:
254
+ return self.context().enqueue(session, payload)
255
+
256
+ def take_outbox(self, session: str) -> list[dict[str, Any]]:
257
+ return self.context().take_outbox(session)
258
+
259
+ def initialize_response(
260
+ self, request_id: Any, protocol: str
261
+ ) -> dict[str, Any]:
262
+ return self.context().initialize_response(request_id, protocol)
263
+
264
+ def cursor_service(self) -> ChannelCursorService:
265
+ return self.context().cursor_service()
266
+
267
+ def write_cursor(self, last_id: int) -> None:
268
+ self.context().write_cursor(last_id)
269
+
270
+ def read_cursor(self) -> int:
271
+ return self.context().read_cursor()
272
+
273
+ def ensure_cursor(self) -> int:
274
+ return self.context().ensure_cursor()
275
+
276
+ def update_cursor(self, last_id: int) -> None:
277
+ self.context().update_cursor(last_id)
278
+
279
+ def resume_policy(self) -> ChannelResumePolicy:
280
+ return self.context().resume_policy()
281
+
282
+ def client_last_event_id(
283
+ self, handler: BaseHTTPRequestHandler
284
+ ) -> int | None:
285
+ return self.context().client_last_event_id(handler)
286
+
287
+ def session_start_last_id(self, handler: BaseHTTPRequestHandler) -> int:
288
+ return self.context().session_start_last_id(handler)
289
+
290
+ def controller(self) -> ChannelMcpHttpController:
291
+ return self.context().controller()
292
+
293
+ def get(self, handler: BaseHTTPRequestHandler, path: str) -> bool:
294
+ return self.context().get(handler, path)
295
+
296
+ def post(
297
+ self,
298
+ handler: BaseHTTPRequestHandler,
299
+ path: str,
300
+ body: dict[str, Any],
301
+ ) -> bool:
302
+ return self.context().post(handler, path, body)
303
+
304
+
305
+ __all__ = [
306
+ "ChannelMcpCompatibilityApi",
307
+ "ChannelMcpContext",
308
+ "ChannelMcpProjectionPorts",
309
+ "ChannelMcpResumePorts",
310
+ "ChannelMcpRpcPorts",
311
+ "ChannelMcpRuntimePorts",
312
+ "ChannelMcpStatePorts",
313
+ ]
@@ -184,3 +184,50 @@ class ChannelMcpDiscoveryService:
184
184
  seen.add(key)
185
185
  unique.append(path)
186
186
  return unique
187
+
188
+
189
+ @dataclass(frozen=True, slots=True)
190
+ class ChannelMcpDiscoveryCompatibilityApi:
191
+ service: Callable[[], ChannelMcpDiscoveryService]
192
+
193
+ def runtime_headers(self, server: dict[str, Any]) -> dict[str, str]:
194
+ return self.service().runtime_headers(server)
195
+
196
+ def servers_from_mapping(self, mapping: Any) -> list[dict[str, Any]]:
197
+ return self.service().servers_from_mapping(mapping)
198
+
199
+ def external_names(
200
+ self,
201
+ passthrough: list[str] | None = None,
202
+ cwd: Path | None = None,
203
+ home: Path | None = None,
204
+ extra_config_paths: list[Path | str] | None = None,
205
+ ) -> list[str]:
206
+ return self.service().external_names(
207
+ passthrough, cwd, home, extra_config_paths
208
+ )
209
+
210
+ def auto_start(
211
+ self,
212
+ passthrough: list[str] | None = None,
213
+ cwd: Path | None = None,
214
+ home: Path | None = None,
215
+ extra_config_paths: list[Path | str] | None = None,
216
+ allowed_server_names: Iterable[str] | None = None,
217
+ include_default_paths: bool = True,
218
+ ) -> list[dict[str, Any]]:
219
+ return self.service().auto_start(
220
+ passthrough,
221
+ cwd,
222
+ home,
223
+ extra_config_paths,
224
+ allowed_server_names,
225
+ include_default_paths,
226
+ )
227
+
228
+
229
+ __all__ = [
230
+ "ChannelMcpDiscoveryCompatibilityApi",
231
+ "ChannelMcpDiscoveryPorts",
232
+ "ChannelMcpDiscoveryService",
233
+ ]