@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.
- package/ciel_runtime.py +2553 -9635
- package/ciel_runtime_support/advisor_request_builder.py +8 -21
- package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
- package/ciel_runtime_support/architecture.py +68 -0
- package/ciel_runtime_support/architecture_budget.py +1 -1
- package/ciel_runtime_support/channel_connection_context.py +233 -0
- package/ciel_runtime_support/channel_delivery_context.py +332 -0
- package/ciel_runtime_support/channel_mcp_context.py +313 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
- package/ciel_runtime_support/channel_mcp_transport.py +5 -1
- package/ciel_runtime_support/channel_message_context.py +212 -0
- package/ciel_runtime_support/channel_message_repository.py +14 -3
- package/ciel_runtime_support/channel_pending_injection.py +9 -0
- package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
- package/ciel_runtime_support/channel_replay_policy.py +38 -0
- package/ciel_runtime_support/channel_runtime_environment.py +8 -0
- package/ciel_runtime_support/channel_session_context.py +236 -0
- package/ciel_runtime_support/channel_terminal_context.py +350 -0
- package/ciel_runtime_support/channel_wake_context.py +532 -0
- package/ciel_runtime_support/claude_environment.py +60 -0
- package/ciel_runtime_support/claude_launch_assembly.py +249 -0
- package/ciel_runtime_support/claude_router.py +62 -12
- package/ciel_runtime_support/cli_application_context.py +132 -0
- package/ciel_runtime_support/cli_assembly.py +50 -0
- package/ciel_runtime_support/codex_backend_context.py +363 -0
- package/ciel_runtime_support/codex_config.py +13 -1
- package/ciel_runtime_support/codex_launch_assembly.py +213 -0
- package/ciel_runtime_support/codex_launch_configuration.py +30 -1
- package/ciel_runtime_support/codex_mcp_integration.py +90 -8
- package/ciel_runtime_support/codex_model_catalog.py +4 -1
- package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
- package/ciel_runtime_support/codex_router.py +38 -8
- package/ciel_runtime_support/codex_turn_recovery.py +154 -0
- package/ciel_runtime_support/config_migrations.py +103 -0
- package/ciel_runtime_support/configuration_cli.py +38 -0
- package/ciel_runtime_support/context_compaction.py +9 -4
- package/ciel_runtime_support/credential_management.py +12 -0
- package/ciel_runtime_support/credentials.py +12 -0
- package/ciel_runtime_support/github_copilot_oauth.py +2 -2
- package/ciel_runtime_support/hosted_formula_tools.py +216 -0
- package/ciel_runtime_support/kimi_runtime_context.py +208 -0
- package/ciel_runtime_support/llm_preset_context.py +338 -0
- package/ciel_runtime_support/managed_mcp_config.py +8 -4
- package/ciel_runtime_support/mcp_configuration_context.py +291 -0
- package/ciel_runtime_support/mcp_http_proxy.py +14 -8
- package/ciel_runtime_support/mcp_probe_transport.py +47 -15
- package/ciel_runtime_support/mcp_transport.py +14 -1
- package/ciel_runtime_support/native_context_recovery.py +72 -0
- package/ciel_runtime_support/ollama_catalog_context.py +213 -0
- package/ciel_runtime_support/ollama_stream_collection.py +103 -0
- package/ciel_runtime_support/ollama_thinking.py +6 -1
- package/ciel_runtime_support/ollama_wire_projection.py +157 -0
- package/ciel_runtime_support/openai_forwarding.py +32 -10
- package/ciel_runtime_support/openai_responses_router.py +12 -0
- package/ciel_runtime_support/package_lifecycle.py +39 -0
- package/ciel_runtime_support/prelaunch_assembly.py +37 -0
- package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
- package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
- package/ciel_runtime_support/prompt_compaction.py +144 -0
- package/ciel_runtime_support/prompt_injection.py +45 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
- package/ciel_runtime_support/protocols/chat_projection.py +85 -5
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
- package/ciel_runtime_support/protocols/ollama_response.py +57 -5
- package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
- package/ciel_runtime_support/protocols/openai_responses.py +61 -15
- package/ciel_runtime_support/provider_adapters.py +26 -0
- package/ciel_runtime_support/provider_administration_context.py +207 -0
- package/ciel_runtime_support/provider_config_mutations.py +3 -0
- package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
- package/ciel_runtime_support/provider_model_context.py +107 -0
- package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
- package/ciel_runtime_support/provider_model_selection.py +10 -3
- package/ciel_runtime_support/provider_models.py +45 -2
- package/ciel_runtime_support/provider_option_cli.py +19 -0
- package/ciel_runtime_support/provider_policy.py +1 -1
- package/ciel_runtime_support/provider_readiness_context.py +189 -0
- package/ciel_runtime_support/provider_request_builder.py +64 -28
- package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
- package/ciel_runtime_support/provider_timeout_policy.py +54 -0
- package/ciel_runtime_support/provider_tool_policy.py +9 -1
- package/ciel_runtime_support/providers/__init__.py +6 -0
- package/ciel_runtime_support/providers/alibaba.py +634 -0
- package/ciel_runtime_support/providers/catalog.py +24 -16
- package/ciel_runtime_support/providers/deepseek.py +73 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
- package/ciel_runtime_support/providers/kimi.py +69 -9
- package/ciel_runtime_support/providers/ollama.py +8 -0
- package/ciel_runtime_support/providers/ollama_context.py +21 -2
- package/ciel_runtime_support/providers/vllm.py +7 -1
- package/ciel_runtime_support/response_collection.py +68 -18
- package/ciel_runtime_support/response_collection_context.py +391 -0
- package/ciel_runtime_support/response_stream_context.py +555 -0
- package/ciel_runtime_support/responses_input_compatibility.py +121 -0
- package/ciel_runtime_support/responses_usage_observer.py +83 -0
- package/ciel_runtime_support/router_client_lifecycle.py +1 -0
- package/ciel_runtime_support/router_http.py +239 -3
- package/ciel_runtime_support/router_observability_context.py +251 -0
- package/ciel_runtime_support/router_process_context.py +200 -0
- package/ciel_runtime_support/router_process_lifecycle.py +2 -0
- package/ciel_runtime_support/router_request_assembly.py +399 -0
- package/ciel_runtime_support/router_request_context.py +215 -0
- package/ciel_runtime_support/router_server_context.py +82 -0
- package/ciel_runtime_support/runaway_output_guard.py +488 -0
- package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
- package/ciel_runtime_support/runtime_asset_context.py +297 -0
- package/ciel_runtime_support/runtime_constants.py +16 -1
- package/ciel_runtime_support/runtime_launch.py +9 -5
- package/ciel_runtime_support/runtime_launch_context.py +130 -0
- package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
- package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
- package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
- package/ciel_runtime_support/runtime_paths.py +60 -40
- package/ciel_runtime_support/runtime_primitives.py +78 -0
- package/ciel_runtime_support/sse_stream_collection.py +236 -0
- package/ciel_runtime_support/statusline_script.py +57 -8
- package/ciel_runtime_support/streaming_anthropic.py +361 -24
- package/ciel_runtime_support/tool_schema.py +40 -2
- package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
- package/ciel_runtime_support/upstream_dump.py +68 -0
- package/ciel_runtime_support/upstream_retry_context.py +259 -0
- package/ciel_runtime_support/workspace_router_selection.py +86 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- 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
|
+
]
|