@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,532 @@
|
|
|
1
|
+
"""Interactive channel wake, transcript, and cursor-recovery bounded context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from . import channel_injection, channel_llm_context
|
|
10
|
+
from .channel_cursor_recovery import (
|
|
11
|
+
ChannelCursorRecoveryPolicy,
|
|
12
|
+
ChannelCursorRecoveryPorts,
|
|
13
|
+
ChannelCursorRecoveryService,
|
|
14
|
+
)
|
|
15
|
+
from .channel_compact_injection import (
|
|
16
|
+
ChannelCompactInjectionService,
|
|
17
|
+
ChannelCompactRequestPorts,
|
|
18
|
+
ChannelCompactRuntimePorts,
|
|
19
|
+
)
|
|
20
|
+
from .channel_pending_injection import (
|
|
21
|
+
ChannelInjectionIO,
|
|
22
|
+
ChannelInjectionPolicy,
|
|
23
|
+
ChannelInjectionPrompts,
|
|
24
|
+
ChannelInjectionServices,
|
|
25
|
+
ChannelInjectionState,
|
|
26
|
+
ChannelInjectionWakeStore,
|
|
27
|
+
inject_pending_channel_messages,
|
|
28
|
+
)
|
|
29
|
+
from .channel_replay_policy import ChannelReplaySafetyPolicy
|
|
30
|
+
from .channel_transcript import (
|
|
31
|
+
ChannelWakeStateReader,
|
|
32
|
+
ChannelWakeStateReaderPorts,
|
|
33
|
+
ChannelWakeTranscriptServices,
|
|
34
|
+
)
|
|
35
|
+
from .channel_transcript_repository import ChannelTranscriptRepository
|
|
36
|
+
from .channel_wake_claim_repository import ChannelWakeClaimRepository
|
|
37
|
+
from .channel_wake_delivery_repository import ChannelWakeDeliveryRepository
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ChannelWakeClaimPorts:
|
|
42
|
+
path: Path
|
|
43
|
+
file_lock: Callable[[Path], Any]
|
|
44
|
+
now: Callable[[], float]
|
|
45
|
+
ttl_seconds: Callable[[], float]
|
|
46
|
+
log: Callable[[str, str], None]
|
|
47
|
+
delivery: ChannelWakeDeliveryRepository
|
|
48
|
+
prompt_message_ids: Callable[[str], set[int]]
|
|
49
|
+
prompt_reference: Callable[[str, int, list[str]], bool]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class ChannelWakeMessagePorts:
|
|
54
|
+
content_to_text: Callable[[Any], str]
|
|
55
|
+
read_messages: Callable[[int, int], list[dict[str, Any]]]
|
|
56
|
+
superseded_ids: Callable[[list[dict[str, Any]]], set[int]]
|
|
57
|
+
wake_request: Callable[[dict[str, Any]], bool]
|
|
58
|
+
plan_mode_active: Callable[[dict[str, Any]], bool]
|
|
59
|
+
delivery_mode: Callable[[], str]
|
|
60
|
+
scan_limit: Callable[[], int]
|
|
61
|
+
skip_reason: Callable[[dict[str, Any]], str]
|
|
62
|
+
remove_wake_prompt: Callable[[dict[str, Any]], dict[str, Any]]
|
|
63
|
+
format_batch_prompt: Callable[[list[dict[str, Any]]], str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class ChannelWakeCursorPorts:
|
|
68
|
+
lock: Callable[[], Any]
|
|
69
|
+
read: Callable[[], int]
|
|
70
|
+
write: Callable[[int], None]
|
|
71
|
+
cache: Callable[[int], None]
|
|
72
|
+
clamp_to_clear_floor: Callable[[int], int]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class ChannelWakeInputPorts:
|
|
77
|
+
environment: dict[str, str]
|
|
78
|
+
platform_enter_bytes: Callable[[], bytes]
|
|
79
|
+
resolve_enter_bytes: Callable[[str | bytes | None, bytes], bytes]
|
|
80
|
+
build_input_bytes: Callable[[str, bytes], bytes]
|
|
81
|
+
find_executable: Callable[[str], str | None]
|
|
82
|
+
run_process: Callable[..., Any]
|
|
83
|
+
sleep: Callable[[float], None]
|
|
84
|
+
retry_delay_seconds: Callable[[], float]
|
|
85
|
+
submit_delay_seconds: Callable[[], float]
|
|
86
|
+
log: Callable[[str, str], None]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class ChannelTranscriptPorts:
|
|
91
|
+
home: Path
|
|
92
|
+
cache: dict[str, Any]
|
|
93
|
+
scope: dict[str, Any]
|
|
94
|
+
recovery_cache: dict[str, Any]
|
|
95
|
+
now: Callable[[], float]
|
|
96
|
+
read_tail: Callable[[Path], str]
|
|
97
|
+
active_tool_call_from_text: Callable[[str], bool]
|
|
98
|
+
active_turn_from_text: Callable[[str], bool]
|
|
99
|
+
queued_age_from_text: Callable[..., float | None]
|
|
100
|
+
wake_state_from_text: Callable[..., str]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class ChannelTranscriptPolicyPorts:
|
|
105
|
+
queued_ids_from_text: Callable[[str, ChannelWakeTranscriptServices], set[int]]
|
|
106
|
+
inflight_stale_seconds: Callable[[], float]
|
|
107
|
+
latest_transcript: Callable[..., Path | None]
|
|
108
|
+
claim_prompt: Callable[[int], str]
|
|
109
|
+
prompt_references_message: Callable[..., bool]
|
|
110
|
+
prompt_message_ids: Callable[[str], set[int]]
|
|
111
|
+
log: Callable[[str, str], None]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class ChannelPendingStatePorts:
|
|
116
|
+
active_tool_call: Callable[[], bool]
|
|
117
|
+
active_turn: Callable[[], bool]
|
|
118
|
+
recover_cursor: Callable[[int], int]
|
|
119
|
+
pending_scan_limit: Callable[[], int]
|
|
120
|
+
superseded_ids: Callable[[list[dict[str, Any]]], set[int]]
|
|
121
|
+
message_is_web_chat: Callable[[dict[str, Any]], bool]
|
|
122
|
+
message_skip_reason: Callable[[dict[str, Any]], str]
|
|
123
|
+
event_identity_key: Callable[[dict[str, Any]], str]
|
|
124
|
+
wake_state_for_message: Callable[..., str]
|
|
125
|
+
queued_wake_is_stale: Callable[..., bool]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True)
|
|
129
|
+
class ChannelPendingDeliveryPorts:
|
|
130
|
+
format_llm_delivery: Callable[[list[dict[str, Any]]], str]
|
|
131
|
+
format_web_chat: Callable[[list[dict[str, Any]]], str]
|
|
132
|
+
format_standard: Callable[[list[dict[str, Any]]], str]
|
|
133
|
+
enter_label: Callable[[bytes], str]
|
|
134
|
+
release_stale: Callable[[int, bool], None]
|
|
135
|
+
mark_delivered: Callable[[int], None]
|
|
136
|
+
record_prompts: Callable[[list[dict[str, Any]], str], None]
|
|
137
|
+
rollback: Callable[[list[dict[str, Any]], list[int]], None]
|
|
138
|
+
commit_cursor: Callable[[int | None], None]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True, slots=True)
|
|
142
|
+
class ChannelPendingIoPorts:
|
|
143
|
+
inject_lock: Any
|
|
144
|
+
read_messages: Callable[..., list[dict[str, Any]]]
|
|
145
|
+
write_prompt: Callable[..., None]
|
|
146
|
+
compact_read: Callable[[], dict[str, Any] | None]
|
|
147
|
+
compact_clear: Callable[[], None]
|
|
148
|
+
messages_path: Path
|
|
149
|
+
log: Callable[[str, str], None]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, slots=True)
|
|
153
|
+
class ChannelPendingPolicyPorts:
|
|
154
|
+
wake_batch_limit: Callable[[], int]
|
|
155
|
+
now: Callable[[], float]
|
|
156
|
+
replay_ttl_seconds: Callable[[], float]
|
|
157
|
+
timestamp_seconds: Callable[[dict[str, Any]], float | None]
|
|
158
|
+
is_web_chat: Callable[[dict[str, Any]], bool]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class ChannelWakeContext:
|
|
163
|
+
claims: ChannelWakeClaimPorts
|
|
164
|
+
messages: ChannelWakeMessagePorts
|
|
165
|
+
cursor: ChannelWakeCursorPorts
|
|
166
|
+
input: ChannelWakeInputPorts
|
|
167
|
+
transcript: ChannelTranscriptPorts
|
|
168
|
+
transcript_policy: ChannelTranscriptPolicyPorts
|
|
169
|
+
pending_state: ChannelPendingStatePorts
|
|
170
|
+
pending_delivery: ChannelPendingDeliveryPorts
|
|
171
|
+
pending_io: ChannelPendingIoPorts
|
|
172
|
+
pending_policy: ChannelPendingPolicyPorts
|
|
173
|
+
|
|
174
|
+
def claim_repository(self) -> ChannelWakeClaimRepository:
|
|
175
|
+
return ChannelWakeClaimRepository(
|
|
176
|
+
path=self.claims.path,
|
|
177
|
+
file_lock=self.claims.file_lock,
|
|
178
|
+
now=self.claims.now,
|
|
179
|
+
ttl_seconds=self.claims.ttl_seconds,
|
|
180
|
+
log=self.claims.log,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def claim_prompt(self, message_id: int) -> str:
|
|
184
|
+
if message_id <= 0:
|
|
185
|
+
return ""
|
|
186
|
+
return self.claims.delivery.prompt(message_id) or self.claim_repository().prompt(
|
|
187
|
+
message_id
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def claim_wake_prompt(self, message_id: int, prompt: str) -> bool:
|
|
191
|
+
return self.claim_repository().claim(message_id, prompt)
|
|
192
|
+
|
|
193
|
+
def clear_wake_claim(self, message_id: int) -> None:
|
|
194
|
+
self.claim_repository().clear(message_id)
|
|
195
|
+
|
|
196
|
+
def prompt_references_message_id(
|
|
197
|
+
self,
|
|
198
|
+
text: str,
|
|
199
|
+
message_id: int,
|
|
200
|
+
prompt_texts: list[str] | tuple[str, ...] | None = None,
|
|
201
|
+
) -> bool:
|
|
202
|
+
prompts = [str(item) for item in (prompt_texts or ()) if str(item or "").strip()]
|
|
203
|
+
if prompt_texts is None:
|
|
204
|
+
claimed_prompt = self.claim_prompt(message_id)
|
|
205
|
+
if claimed_prompt:
|
|
206
|
+
prompts.append(claimed_prompt)
|
|
207
|
+
return self.claims.prompt_reference(text, message_id, prompts)
|
|
208
|
+
|
|
209
|
+
def message_ids_already_in_request(self, body: dict[str, Any]) -> set[int]:
|
|
210
|
+
ids: set[int] = set()
|
|
211
|
+
for message in body.get("messages") or []:
|
|
212
|
+
if not isinstance(message, dict):
|
|
213
|
+
continue
|
|
214
|
+
text = self.messages.content_to_text(message.get("content"))
|
|
215
|
+
if (
|
|
216
|
+
"ciel-runtime external channel message" not in text
|
|
217
|
+
and "[external channel input]" not in text
|
|
218
|
+
):
|
|
219
|
+
continue
|
|
220
|
+
ids.update(self.claims.prompt_message_ids(text))
|
|
221
|
+
return ids
|
|
222
|
+
|
|
223
|
+
def commit_cursor(self, last_id: int) -> None:
|
|
224
|
+
self.cursor.cache(last_id)
|
|
225
|
+
try:
|
|
226
|
+
self.cursor.write(last_id)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
self.claims.log(
|
|
229
|
+
"WARN",
|
|
230
|
+
f"channel_llm_cursor_write_failed error={type(exc).__name__}: {exc}",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def stdin_skip_reason(self, message_id: int) -> str:
|
|
234
|
+
if self.claims.delivery.is_delivered(message_id):
|
|
235
|
+
return "stdin_wake_delivered"
|
|
236
|
+
return (
|
|
237
|
+
"stdin_wake_claimed"
|
|
238
|
+
if self.transcript_policy.claim_prompt(message_id)
|
|
239
|
+
else ""
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def body_with_pending_messages(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
243
|
+
return channel_llm_context.inject_pending_channel_context(
|
|
244
|
+
body,
|
|
245
|
+
channel_llm_context.ChannelLlmContextServices(
|
|
246
|
+
policy=channel_llm_context.ChannelLlmContextPolicy(
|
|
247
|
+
wake_request=self.messages.wake_request,
|
|
248
|
+
plan_mode_active=self.messages.plan_mode_active,
|
|
249
|
+
delivery_mode=self.messages.delivery_mode,
|
|
250
|
+
ids_in_request=self.message_ids_already_in_request,
|
|
251
|
+
scan_limit=self.messages.scan_limit,
|
|
252
|
+
skip_reason=self.messages.skip_reason,
|
|
253
|
+
stdin_skip_reason=self.stdin_skip_reason,
|
|
254
|
+
),
|
|
255
|
+
repository=channel_llm_context.ChannelLlmContextRepository(
|
|
256
|
+
lock=self.cursor.lock,
|
|
257
|
+
read_cursor=self.cursor.read,
|
|
258
|
+
commit_cursor=self.commit_cursor,
|
|
259
|
+
read_messages=self.messages.read_messages,
|
|
260
|
+
superseded_ids=self.messages.superseded_ids,
|
|
261
|
+
),
|
|
262
|
+
projection=channel_llm_context.ChannelLlmContextProjection(
|
|
263
|
+
remove_wake_prompt=self.messages.remove_wake_prompt,
|
|
264
|
+
format_prompt=self.messages.format_batch_prompt,
|
|
265
|
+
),
|
|
266
|
+
log=self.claims.log,
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def write_all(fd: Any, data: bytes) -> None:
|
|
272
|
+
writer = getattr(fd, "write", None)
|
|
273
|
+
if callable(writer):
|
|
274
|
+
writer(data)
|
|
275
|
+
return
|
|
276
|
+
import os
|
|
277
|
+
|
|
278
|
+
view = memoryview(data)
|
|
279
|
+
while view:
|
|
280
|
+
view = view[os.write(fd, view) :]
|
|
281
|
+
|
|
282
|
+
def enter_bytes(self, value: str | bytes | None = None) -> bytes:
|
|
283
|
+
configured = (
|
|
284
|
+
self.input.environment.get("CIEL_RUNTIME_CHANNEL_WAKE_ENTER")
|
|
285
|
+
if value is None
|
|
286
|
+
else value
|
|
287
|
+
)
|
|
288
|
+
return self.input.resolve_enter_bytes(
|
|
289
|
+
configured, self.input.platform_enter_bytes()
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def input_bytes(self, prompt: str, enter_bytes: bytes | None = None) -> bytes:
|
|
293
|
+
return self.input.build_input_bytes(prompt, self.enter_bytes(enter_bytes))
|
|
294
|
+
|
|
295
|
+
def current_tmux_pane_text(self) -> str | None:
|
|
296
|
+
from .channel_terminal_input import TmuxPaneSnapshot
|
|
297
|
+
|
|
298
|
+
return TmuxPaneSnapshot(
|
|
299
|
+
self.input.environment,
|
|
300
|
+
self.input.find_executable,
|
|
301
|
+
self.input.run_process,
|
|
302
|
+
self.input.log,
|
|
303
|
+
).capture()
|
|
304
|
+
|
|
305
|
+
def write_prompt(
|
|
306
|
+
self,
|
|
307
|
+
master_fd: int,
|
|
308
|
+
prompt: str,
|
|
309
|
+
enter_bytes: bytes | None = None,
|
|
310
|
+
*,
|
|
311
|
+
submit_retry_count: int = 1,
|
|
312
|
+
confirm_submit: bool = False,
|
|
313
|
+
bracketed_paste: bool = False,
|
|
314
|
+
submit_delay_seconds: float | None = None,
|
|
315
|
+
write_all: Callable[[Any, bytes], None] | None = None,
|
|
316
|
+
snapshot: Callable[[], str | None] | None = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
delay = (
|
|
319
|
+
self.input.submit_delay_seconds()
|
|
320
|
+
if submit_delay_seconds is None
|
|
321
|
+
else max(0.0, float(submit_delay_seconds))
|
|
322
|
+
)
|
|
323
|
+
injector = channel_injection.ChannelPromptInjector(
|
|
324
|
+
sleep=self.input.sleep,
|
|
325
|
+
retry_delay_seconds=self.input.retry_delay_seconds,
|
|
326
|
+
snapshot=snapshot or self.current_tmux_pane_text,
|
|
327
|
+
log=self.input.log,
|
|
328
|
+
)
|
|
329
|
+
injector.inject(
|
|
330
|
+
channel_injection.CallableInputTransport(
|
|
331
|
+
master_fd, write_all or self.write_all
|
|
332
|
+
),
|
|
333
|
+
channel_injection.PromptInjection(
|
|
334
|
+
prompt=prompt,
|
|
335
|
+
policy=channel_injection.RuntimeInjectionPolicy(
|
|
336
|
+
runtime="interactive-cli",
|
|
337
|
+
clear_input=b"\x15",
|
|
338
|
+
submit_input=self.enter_bytes(enter_bytes),
|
|
339
|
+
submit_delay_seconds=delay,
|
|
340
|
+
submit_attempts=max(1, min(8, int(submit_retry_count or 1))),
|
|
341
|
+
confirm_submission=confirm_submit,
|
|
342
|
+
bracketed_paste=bracketed_paste,
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def transcript_repository(self) -> ChannelTranscriptRepository:
|
|
348
|
+
return ChannelTranscriptRepository(
|
|
349
|
+
self.transcript.home,
|
|
350
|
+
self.transcript.cache,
|
|
351
|
+
self.transcript.scope,
|
|
352
|
+
self.transcript.now,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def set_transcript_scope(
|
|
356
|
+
self,
|
|
357
|
+
runtime: str,
|
|
358
|
+
*,
|
|
359
|
+
started_at: float | None = None,
|
|
360
|
+
codex_home: Path | None = None,
|
|
361
|
+
) -> None:
|
|
362
|
+
self.transcript_repository().set_scope(
|
|
363
|
+
runtime, started_at=started_at, codex_home=codex_home
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def transcript_roots(self) -> tuple[tuple[Path, str], ...]:
|
|
367
|
+
return self.transcript_repository().roots()
|
|
368
|
+
|
|
369
|
+
def latest_transcript_path(self, ttl_seconds: float = 2.0) -> Path | None:
|
|
370
|
+
return self.transcript_repository().latest(ttl_seconds)
|
|
371
|
+
|
|
372
|
+
def transcript_services(self) -> ChannelWakeTranscriptServices:
|
|
373
|
+
return ChannelWakeTranscriptServices(
|
|
374
|
+
claim_prompt=self.transcript_policy.claim_prompt,
|
|
375
|
+
prompt_references_message_id=self.transcript_policy.prompt_references_message,
|
|
376
|
+
prompt_message_ids=self.transcript_policy.prompt_message_ids,
|
|
377
|
+
now=self.transcript.now,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def queued_age_seconds_from_text(
|
|
381
|
+
self,
|
|
382
|
+
message_id: int,
|
|
383
|
+
text: str,
|
|
384
|
+
prompt_texts: list[str] | tuple[str, ...] | None = None,
|
|
385
|
+
*,
|
|
386
|
+
now: float | None = None,
|
|
387
|
+
) -> float | None:
|
|
388
|
+
return self.transcript.queued_age_from_text(
|
|
389
|
+
message_id, text, prompt_texts, self.transcript_services(), now=now
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def wake_state_from_text(
|
|
393
|
+
self,
|
|
394
|
+
message_id: int,
|
|
395
|
+
text: str,
|
|
396
|
+
prompt_texts: list[str] | tuple[str, ...] | None = None,
|
|
397
|
+
) -> str:
|
|
398
|
+
return self.transcript.wake_state_from_text(
|
|
399
|
+
message_id, text, prompt_texts, self.transcript_services()
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def wake_state_reader(self) -> ChannelWakeStateReader:
|
|
403
|
+
return ChannelWakeStateReader(
|
|
404
|
+
ChannelWakeStateReaderPorts(
|
|
405
|
+
self.transcript_policy.latest_transcript,
|
|
406
|
+
self.transcript.read_tail,
|
|
407
|
+
self.wake_state_from_text,
|
|
408
|
+
self.queued_age_seconds_from_text,
|
|
409
|
+
self.transcript_policy.inflight_stale_seconds,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def active_tool_call(self) -> bool:
|
|
414
|
+
path = self.transcript_policy.latest_transcript()
|
|
415
|
+
text = self.transcript.read_tail(path) if path is not None else ""
|
|
416
|
+
return bool(text and self.transcript.active_tool_call_from_text(text))
|
|
417
|
+
|
|
418
|
+
def active_turn(self) -> bool:
|
|
419
|
+
path = self.transcript_policy.latest_transcript()
|
|
420
|
+
text = self.transcript.read_tail(path) if path is not None else ""
|
|
421
|
+
return bool(text and self.transcript.active_turn_from_text(text))
|
|
422
|
+
|
|
423
|
+
def queued_command_ids_from_text(self, text: str) -> set[int]:
|
|
424
|
+
return self.transcript_policy.queued_ids_from_text(
|
|
425
|
+
text, self.transcript_services()
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def cursor_recovery_service(self) -> ChannelCursorRecoveryService:
|
|
429
|
+
return ChannelCursorRecoveryService(
|
|
430
|
+
cache=self.transcript.recovery_cache,
|
|
431
|
+
policy=ChannelCursorRecoveryPolicy(),
|
|
432
|
+
ports=ChannelCursorRecoveryPorts(
|
|
433
|
+
latest_transcript=self.transcript_policy.latest_transcript,
|
|
434
|
+
read_tail=self.transcript.read_tail,
|
|
435
|
+
queued_command_ids=self.queued_command_ids_from_text,
|
|
436
|
+
wake_state=self.wake_state_from_text,
|
|
437
|
+
clamp_to_clear_floor=self.cursor.clamp_to_clear_floor,
|
|
438
|
+
now=self.transcript.now,
|
|
439
|
+
log=self.transcript_policy.log,
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def pending_injection_services(self) -> ChannelInjectionServices:
|
|
444
|
+
return ChannelInjectionServices(
|
|
445
|
+
state=ChannelInjectionState(
|
|
446
|
+
active_tool_call=self.pending_state.active_tool_call,
|
|
447
|
+
active_turn=self.pending_state.active_turn,
|
|
448
|
+
recover_cursor=self.pending_state.recover_cursor,
|
|
449
|
+
pending_scan_limit=self.pending_state.pending_scan_limit,
|
|
450
|
+
superseded_ids=self.pending_state.superseded_ids,
|
|
451
|
+
message_is_web_chat=self.pending_state.message_is_web_chat,
|
|
452
|
+
message_skip_reason=self.pending_state.message_skip_reason,
|
|
453
|
+
event_identity_key=self.pending_state.event_identity_key,
|
|
454
|
+
wake_state_for_message=self.pending_state.wake_state_for_message,
|
|
455
|
+
queued_wake_is_stale=self.pending_state.queued_wake_is_stale,
|
|
456
|
+
),
|
|
457
|
+
prompts=ChannelInjectionPrompts(
|
|
458
|
+
llm_delivery=self.pending_delivery.format_llm_delivery,
|
|
459
|
+
web_chat=self.pending_delivery.format_web_chat,
|
|
460
|
+
standard=self.pending_delivery.format_standard,
|
|
461
|
+
enter_bytes=self.enter_bytes,
|
|
462
|
+
enter_label=self.pending_delivery.enter_label,
|
|
463
|
+
),
|
|
464
|
+
wake_store=ChannelInjectionWakeStore(
|
|
465
|
+
claim_for_nonblocking_scan=self.transcript_policy.claim_prompt,
|
|
466
|
+
claim_prompt=self.claim_wake_prompt,
|
|
467
|
+
clear_claim=self.clear_wake_claim,
|
|
468
|
+
release_stale=self.pending_delivery.release_stale,
|
|
469
|
+
mark_delivered=self.pending_delivery.mark_delivered,
|
|
470
|
+
record_prompts=self.pending_delivery.record_prompts,
|
|
471
|
+
rollback=self.pending_delivery.rollback,
|
|
472
|
+
commit_cursor=self.pending_delivery.commit_cursor,
|
|
473
|
+
),
|
|
474
|
+
io=ChannelInjectionIO(
|
|
475
|
+
inject_lock=self.pending_io.inject_lock,
|
|
476
|
+
read_messages=self.pending_io.read_messages,
|
|
477
|
+
write_prompt=self.pending_io.write_prompt,
|
|
478
|
+
log=self.pending_io.log,
|
|
479
|
+
),
|
|
480
|
+
policy=ChannelInjectionPolicy(
|
|
481
|
+
wake_batch_limit=self.pending_policy.wake_batch_limit,
|
|
482
|
+
replay_skip_reason=ChannelReplaySafetyPolicy(
|
|
483
|
+
self.pending_policy.now,
|
|
484
|
+
self.pending_policy.replay_ttl_seconds,
|
|
485
|
+
self.pending_policy.timestamp_seconds,
|
|
486
|
+
self.pending_policy.is_web_chat,
|
|
487
|
+
).skip_reason,
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def inject_pending(
|
|
492
|
+
self,
|
|
493
|
+
master_fd: int,
|
|
494
|
+
last_id: int,
|
|
495
|
+
enter_bytes: bytes | None = None,
|
|
496
|
+
**options: Any,
|
|
497
|
+
) -> int:
|
|
498
|
+
return inject_pending_channel_messages(
|
|
499
|
+
master_fd,
|
|
500
|
+
last_id,
|
|
501
|
+
enter_bytes,
|
|
502
|
+
services=self.pending_injection_services(),
|
|
503
|
+
**options,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def inject_compact(
|
|
507
|
+
self,
|
|
508
|
+
master_fd: int,
|
|
509
|
+
enter_bytes: bytes | None = None,
|
|
510
|
+
**options: Any,
|
|
511
|
+
) -> str:
|
|
512
|
+
return ChannelCompactInjectionService(
|
|
513
|
+
request=ChannelCompactRequestPorts(
|
|
514
|
+
read=self.pending_io.compact_read,
|
|
515
|
+
clear=self.pending_io.compact_clear,
|
|
516
|
+
),
|
|
517
|
+
runtime=ChannelCompactRuntimePorts(
|
|
518
|
+
active_tool_call=self.pending_state.active_tool_call,
|
|
519
|
+
active_turn=self.pending_state.active_turn,
|
|
520
|
+
enter_bytes=self.enter_bytes,
|
|
521
|
+
write_prompt=self.pending_io.write_prompt,
|
|
522
|
+
enter_label=self.pending_delivery.enter_label,
|
|
523
|
+
),
|
|
524
|
+
log=self.pending_io.log,
|
|
525
|
+
).inject(master_fd, enter_bytes, **options)
|
|
526
|
+
|
|
527
|
+
def messages_file_marker(self) -> tuple[float, int]:
|
|
528
|
+
try:
|
|
529
|
+
stat = self.pending_io.messages_path.stat()
|
|
530
|
+
return (stat.st_mtime, stat.st_size)
|
|
531
|
+
except Exception:
|
|
532
|
+
return (0.0, 0)
|
|
@@ -178,6 +178,65 @@ class ClaudeModelAliasPolicy:
|
|
|
178
178
|
return result
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class ClaudeModelAliasCompatibilityApi:
|
|
183
|
+
policy: Callable[[], ClaudeModelAliasPolicy]
|
|
184
|
+
context_limit: Callable[[str, dict[str, Any]], int | None]
|
|
185
|
+
|
|
186
|
+
def claims_one_million_context(
|
|
187
|
+
self,
|
|
188
|
+
provider: str,
|
|
189
|
+
config: dict[str, Any],
|
|
190
|
+
model: str,
|
|
191
|
+
*,
|
|
192
|
+
include_current: bool = True,
|
|
193
|
+
) -> bool:
|
|
194
|
+
return self.policy().claims_one_million_context(
|
|
195
|
+
provider,
|
|
196
|
+
config,
|
|
197
|
+
model,
|
|
198
|
+
include_current=include_current,
|
|
199
|
+
context_limit=(
|
|
200
|
+
self.context_limit(provider, config) if include_current else None
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def context_model_alias(
|
|
205
|
+
self,
|
|
206
|
+
provider: str,
|
|
207
|
+
config: dict[str, Any],
|
|
208
|
+
model: str,
|
|
209
|
+
upstream_model: str | None = None,
|
|
210
|
+
) -> str:
|
|
211
|
+
return self.policy().context_model_alias(
|
|
212
|
+
provider,
|
|
213
|
+
config,
|
|
214
|
+
model,
|
|
215
|
+
upstream_model,
|
|
216
|
+
context_limit=(
|
|
217
|
+
self.context_limit(provider, config)
|
|
218
|
+
if upstream_model is None
|
|
219
|
+
else None
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def matches_family(self, model_id: str, family: str) -> bool:
|
|
224
|
+
return self.policy().matches_family(model_id, family)
|
|
225
|
+
|
|
226
|
+
def default_model_aliases(
|
|
227
|
+
self,
|
|
228
|
+
provider: str,
|
|
229
|
+
config: dict[str, Any],
|
|
230
|
+
current_model_alias: str,
|
|
231
|
+
) -> dict[str, str]:
|
|
232
|
+
return self.policy().default_model_aliases(
|
|
233
|
+
provider,
|
|
234
|
+
config,
|
|
235
|
+
current_model_alias,
|
|
236
|
+
context_limit=self.context_limit(provider, config),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
181
240
|
@dataclass(frozen=True)
|
|
182
241
|
class ClaudeEnvironmentSourcePorts:
|
|
183
242
|
load_config: Callable[[], dict[str, Any]]
|
|
@@ -373,6 +432,7 @@ __all__ = [
|
|
|
373
432
|
"ClaudeLimitPolicy",
|
|
374
433
|
"ClaudeLimitPorts",
|
|
375
434
|
"ClaudeModelAliasPolicy",
|
|
435
|
+
"ClaudeModelAliasCompatibilityApi",
|
|
376
436
|
"ClaudeModelPorts",
|
|
377
437
|
"ClaudeRuntimeSettingsPolicy",
|
|
378
438
|
"ClaudeRuntimeSettingsPorts",
|