@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

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 (136) hide show
  1. package/ciel_runtime.py +2555 -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/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -0,0 +1,394 @@
1
+ """Prelaunch terminal shell and provider launch-preference 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, Mapping
8
+
9
+ from .architecture import ProviderAdapter, ProviderConfig, ProviderUiPolicy
10
+ from .prelaunch_terminal import (
11
+ PrelaunchInputStyle,
12
+ PrelaunchRenderServices,
13
+ TerminalSelectionServices,
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class PrelaunchVisualPorts:
19
+ ansi: Callable[[str, str], str]
20
+ fit_cells: Callable[[Any, int], str]
21
+ stdout_isatty: Callable[[], bool]
22
+ render_intro: Callable[[int, str, str], list[str]]
23
+ app_name: str
24
+ credits: str
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class PrelaunchInputPorts:
29
+ enable_ansi: Callable[[], None]
30
+ status_lines: Callable[[], list[str]]
31
+ read_terminal_key: Callable[..., str]
32
+ write_debug: Callable[[Path, str], None]
33
+ debug_path: Path
34
+ run_select: Callable[..., int | None]
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class PrelaunchProviderPorts:
39
+ configured_adapter: Callable[[str, dict[str, Any]], ProviderAdapter]
40
+ contract_config: Callable[[str, dict[str, Any]], ProviderConfig]
41
+ labels: Mapping[str, str]
42
+ supports_runtime: Callable[[str, str], bool]
43
+ load_config: Callable[[], dict[str, Any]]
44
+ preferred_action: Callable[
45
+ [
46
+ dict[str, Any],
47
+ str,
48
+ Callable[[str], bool],
49
+ Callable[[str], bool],
50
+ Callable[[str], bool],
51
+ ],
52
+ str,
53
+ ]
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class PrelaunchPromptPorts:
58
+ render_services: PrelaunchRenderServices
59
+ input_style: PrelaunchInputStyle
60
+ render_screen: Callable[..., bool]
61
+ read_value_raw: Callable[..., str | None]
62
+ read_value: Callable[..., str]
63
+ read_multiline_raw: Callable[..., str | None]
64
+ read_multiline: Callable[..., str]
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class PrelaunchShellContext:
69
+ visual: PrelaunchVisualPorts
70
+ input: PrelaunchInputPorts
71
+ provider: PrelaunchProviderPorts
72
+ prompt: PrelaunchPromptPorts
73
+ main_menu_actions: tuple[str, ...]
74
+
75
+ def color_line(self, text: str, code: str, width: int) -> str:
76
+ return self.visual.ansi(self.visual.fit_cells(text, width), code)
77
+
78
+ def clean_render_lines(self, lines: list[str], width: int) -> list[str]:
79
+ return [self.visual.fit_cells(line, width) for line in lines]
80
+
81
+ def clear_screen(self) -> None:
82
+ if self.visual.stdout_isatty():
83
+ print("\033[2J\033[H", end="")
84
+
85
+ def intro_panel_lines(self, width: int) -> list[str]:
86
+ return self.visual.render_intro(
87
+ width, self.visual.app_name, self.visual.credits
88
+ )
89
+
90
+ def print_intro_panel(self, width: int) -> None:
91
+ print("\n".join(self.intro_panel_lines(width)))
92
+
93
+ def append_menu_key_debug_log(self, line: str) -> None:
94
+ self.input.write_debug(self.input.debug_path, line)
95
+
96
+ def read_menu_key(self, fd: int | None = None) -> str:
97
+ return self.input.read_terminal_key(
98
+ fd, debug_log=self.append_menu_key_debug_log
99
+ )
100
+
101
+ def portable_select(
102
+ self,
103
+ title: str,
104
+ rows: list[str],
105
+ current: int = 0,
106
+ footer: str = "",
107
+ info_lines: list[str] | None = None,
108
+ show_intro: bool = False,
109
+ ) -> int | None:
110
+ return self.input.run_select(
111
+ title,
112
+ rows,
113
+ current,
114
+ footer,
115
+ info_lines,
116
+ show_intro,
117
+ services=TerminalSelectionServices(
118
+ enable_ansi=self.input.enable_ansi,
119
+ ansi=self.visual.ansi,
120
+ intro_panel_lines=self.intro_panel_lines,
121
+ status_lines=self.input.status_lines,
122
+ read_key=self.read_menu_key,
123
+ ),
124
+ )
125
+
126
+ def compact_text(self, value: Any, width: int = 72) -> str:
127
+ return self.visual.fit_cells(value, width)
128
+
129
+ def provider_ui_policy(
130
+ self, provider: str, config: dict[str, Any]
131
+ ) -> ProviderUiPolicy:
132
+ adapter = self.provider.configured_adapter(provider, config)
133
+ return adapter.ui_policy(self.provider.contract_config(provider, config))
134
+
135
+ def provider_menu_label(self, provider: str, config: dict[str, Any]) -> str:
136
+ policy = self.provider_ui_policy(provider, config)
137
+ if config.get("route_through_router") and policy.routed_menu_label:
138
+ return policy.routed_menu_label
139
+ return policy.menu_label or self.provider.labels.get(provider, provider)
140
+
141
+ def current_provider_panel_choice(
142
+ self, provider: str, config: dict[str, Any]
143
+ ) -> str:
144
+ policy = self.provider_ui_policy(provider, config)
145
+ if config.get("route_through_router") and policy.routed_choice:
146
+ return policy.routed_choice
147
+ return policy.native_choice or provider
148
+
149
+ def launch_enabled(self, runtime: str, provider: str) -> bool:
150
+ return self.provider.supports_runtime(runtime, provider)
151
+
152
+ def default_prelaunch_action(self, provider: str) -> str:
153
+ config = self.provider.load_config()
154
+ if provider == "kimi":
155
+ remembered = str(config.get("last_launch_action") or "").strip()
156
+ if remembered == "launch":
157
+ return remembered
158
+ if remembered in {"launch-codex", "launch-codex-app-server"}:
159
+ return remembered
160
+ return "launch-kimi"
161
+ return self.provider.preferred_action(
162
+ config,
163
+ provider,
164
+ lambda name: self.launch_enabled("agy", name),
165
+ lambda name: self.launch_enabled("claude", name),
166
+ lambda name: self.launch_enabled("codex", name),
167
+ )
168
+
169
+ def prelaunch_action_index(self, action: str) -> int:
170
+ try:
171
+ return self.main_menu_actions.index(action)
172
+ except ValueError:
173
+ return 0
174
+
175
+ def prelaunch_render_services(self) -> PrelaunchRenderServices:
176
+ return self.prompt.render_services
177
+
178
+ def prelaunch_input_style(self) -> PrelaunchInputStyle:
179
+ return self.prompt.input_style
180
+
181
+ def render_prelaunch_screen(
182
+ self,
183
+ main_index: int,
184
+ panel: str | None,
185
+ panel_index: int,
186
+ panel_rows: list[str],
187
+ checks: list[str],
188
+ messages: list[str],
189
+ first_render: bool,
190
+ ) -> bool:
191
+ return self.prompt.render_screen(
192
+ main_index,
193
+ panel,
194
+ panel_index,
195
+ panel_rows,
196
+ checks,
197
+ messages,
198
+ first_render,
199
+ services=self.prompt.render_services,
200
+ )
201
+
202
+ def prompt_menu_value_raw(
203
+ self, label: str, default: str = "", secret: bool = False
204
+ ) -> str | None:
205
+ return self.prompt.read_value_raw(
206
+ label, default, secret, style=self.prompt.input_style
207
+ )
208
+
209
+ def prompt_menu_value(
210
+ self,
211
+ prompt: str,
212
+ default: str = "",
213
+ secret: bool = False,
214
+ restore_tty: Callable[[], None] | None = None,
215
+ raw_tty: Callable[[], None] | None = None,
216
+ ) -> str:
217
+ return self.prompt.read_value(
218
+ prompt,
219
+ default,
220
+ secret,
221
+ restore_tty,
222
+ raw_tty,
223
+ style=self.prompt.input_style,
224
+ )
225
+
226
+ def prompt_menu_multiline_value_raw(
227
+ self, label: str, secret: bool = False
228
+ ) -> str | None:
229
+ return self.prompt.read_multiline_raw(
230
+ label, secret, style=self.prompt.input_style
231
+ )
232
+
233
+ def prompt_menu_multiline_value(
234
+ self,
235
+ prompt: str,
236
+ restore_tty: Callable[[], None] | None = None,
237
+ raw_tty: Callable[[], None] | None = None,
238
+ secret: bool = True,
239
+ ) -> str:
240
+ return self.prompt.read_multiline(
241
+ prompt,
242
+ restore_tty,
243
+ raw_tty,
244
+ secret,
245
+ style=self.prompt.input_style,
246
+ )
247
+
248
+
249
+ @dataclass(frozen=True, slots=True)
250
+ class PrelaunchShellCompatibilityApi:
251
+ context: Callable[[], PrelaunchShellContext]
252
+
253
+ def color_line(self, text: str, code: str, width: int) -> str:
254
+ return self.context().color_line(text, code, width)
255
+
256
+ def clean_render_lines(self, lines: list[str], width: int) -> list[str]:
257
+ return self.context().clean_render_lines(lines, width)
258
+
259
+ def clear_screen(self) -> None:
260
+ self.context().clear_screen()
261
+
262
+ def intro_panel_lines(self, width: int) -> list[str]:
263
+ return self.context().intro_panel_lines(width)
264
+
265
+ def print_intro_panel(self, width: int) -> None:
266
+ self.context().print_intro_panel(width)
267
+
268
+ def append_menu_key_debug_log(self, line: str) -> None:
269
+ self.context().append_menu_key_debug_log(line)
270
+
271
+ def read_menu_key(self, fd: int | None = None) -> str:
272
+ return self.context().read_menu_key(fd)
273
+
274
+ def portable_select(
275
+ self,
276
+ title: str,
277
+ rows: list[str],
278
+ current: int = 0,
279
+ footer: str = "",
280
+ info_lines: list[str] | None = None,
281
+ show_intro: bool = False,
282
+ ) -> int | None:
283
+ return self.context().portable_select(
284
+ title, rows, current, footer, info_lines, show_intro
285
+ )
286
+
287
+ def compact_text(self, value: Any, width: int = 72) -> str:
288
+ return self.context().compact_text(value, width)
289
+
290
+ def provider_ui_policy(
291
+ self, provider: str, config: dict[str, Any]
292
+ ) -> ProviderUiPolicy:
293
+ return self.context().provider_ui_policy(provider, config)
294
+
295
+ def provider_menu_label(self, provider: str, config: dict[str, Any]) -> str:
296
+ return self.context().provider_menu_label(provider, config)
297
+
298
+ def current_provider_panel_choice(
299
+ self, provider: str, config: dict[str, Any]
300
+ ) -> str:
301
+ return self.context().current_provider_panel_choice(provider, config)
302
+
303
+ def claude_launch_enabled(
304
+ self, provider: str, config: dict[str, Any] | None = None
305
+ ) -> bool:
306
+ del config
307
+ return self.context().launch_enabled("claude", provider)
308
+
309
+ def agy_launch_enabled(
310
+ self, provider: str, config: dict[str, Any] | None = None
311
+ ) -> bool:
312
+ del config
313
+ return self.context().launch_enabled("agy", provider)
314
+
315
+ def codex_launch_enabled(
316
+ self, provider: str, config: dict[str, Any] | None = None
317
+ ) -> bool:
318
+ del config
319
+ return self.context().launch_enabled("codex", provider)
320
+
321
+ def default_prelaunch_action(self, provider: str) -> str:
322
+ return self.context().default_prelaunch_action(provider)
323
+
324
+ def prelaunch_action_index(self, action: str) -> int:
325
+ return self.context().prelaunch_action_index(action)
326
+
327
+ def prelaunch_render_services(self) -> PrelaunchRenderServices:
328
+ return self.context().prelaunch_render_services()
329
+
330
+ def prelaunch_input_style(self) -> PrelaunchInputStyle:
331
+ return self.context().prelaunch_input_style()
332
+
333
+ def render_prelaunch_screen(
334
+ self,
335
+ main_index: int,
336
+ panel: str | None,
337
+ panel_index: int,
338
+ panel_rows: list[str],
339
+ checks: list[str],
340
+ messages: list[str],
341
+ first_render: bool,
342
+ ) -> bool:
343
+ return self.context().render_prelaunch_screen(
344
+ main_index,
345
+ panel,
346
+ panel_index,
347
+ panel_rows,
348
+ checks,
349
+ messages,
350
+ first_render,
351
+ )
352
+
353
+ def prompt_menu_value_raw(
354
+ self, label: str, default: str = "", secret: bool = False
355
+ ) -> str | None:
356
+ return self.context().prompt_menu_value_raw(label, default, secret)
357
+
358
+ def prompt_menu_value(
359
+ self,
360
+ prompt: str,
361
+ default: str = "",
362
+ secret: bool = False,
363
+ restore_tty: Callable[[], None] | None = None,
364
+ raw_tty: Callable[[], None] | None = None,
365
+ ) -> str:
366
+ return self.context().prompt_menu_value(
367
+ prompt, default, secret, restore_tty, raw_tty
368
+ )
369
+
370
+ def prompt_menu_multiline_value_raw(
371
+ self, label: str, secret: bool = False
372
+ ) -> str | None:
373
+ return self.context().prompt_menu_multiline_value_raw(label, secret)
374
+
375
+ def prompt_menu_multiline_value(
376
+ self,
377
+ prompt: str,
378
+ restore_tty: Callable[[], None] | None = None,
379
+ raw_tty: Callable[[], None] | None = None,
380
+ secret: bool = True,
381
+ ) -> str:
382
+ return self.context().prompt_menu_multiline_value(
383
+ prompt, restore_tty, raw_tty, secret
384
+ )
385
+
386
+
387
+ __all__ = [
388
+ "PrelaunchInputPorts",
389
+ "PrelaunchProviderPorts",
390
+ "PrelaunchPromptPorts",
391
+ "PrelaunchShellCompatibilityApi",
392
+ "PrelaunchShellContext",
393
+ "PrelaunchVisualPorts",
394
+ ]
@@ -181,6 +181,150 @@ def compact_chat_messages_for_budget(
181
181
  return compacted
182
182
 
183
183
 
184
+ RESPONSES_CALL_TYPES = ("function_call", "custom_tool_call")
185
+ RESPONSES_OUTPUT_TYPES = ("function_call_output", "custom_tool_call_output")
186
+
187
+
188
+ def responses_item_as_message(item: dict[str, Any]) -> dict[str, Any]:
189
+ """Project one Responses input item into the shape the summary builder reads.
190
+
191
+ The chunked context-guard summary is written against ``role``/``content``
192
+ messages. Responses carries the same conversation as typed items, so the
193
+ projection exists only so one summariser serves both wires.
194
+ """
195
+
196
+ item_type = str(item.get("type") or "")
197
+ if item_type == "message":
198
+ return {"role": str(item.get("role") or "user"), "content": item.get("content")}
199
+ if item_type in RESPONSES_CALL_TYPES:
200
+ arguments = item.get("arguments")
201
+ if arguments is None:
202
+ arguments = item.get("input")
203
+ return {
204
+ "role": "assistant",
205
+ "content": f"{item.get('name') or item_type}({arguments if isinstance(arguments, str) else ''})",
206
+ }
207
+ if item_type in RESPONSES_OUTPUT_TYPES:
208
+ return {"role": "tool", "content": item.get("output")}
209
+ return {"role": "assistant", "content": item_type}
210
+
211
+
212
+ def responses_tail_is_safe(items: list[dict[str, Any]], start: int) -> bool:
213
+ """Report whether cutting before ``start`` leaves every kept pair intact.
214
+
215
+ A tool output replayed without the call it answers is not valid Responses
216
+ input, so a tail may not begin inside a call/output pair.
217
+ """
218
+
219
+ retained_calls = {
220
+ str(item.get("call_id") or "")
221
+ for item in items[start:]
222
+ if str(item.get("type") or "") in RESPONSES_CALL_TYPES
223
+ }
224
+ return not any(
225
+ str(item.get("type") or "") in RESPONSES_OUTPUT_TYPES
226
+ and str(item.get("call_id") or "") not in retained_calls
227
+ for item in items[start:]
228
+ )
229
+
230
+
231
+ def compact_responses_input_for_budget(
232
+ body: dict[str, Any],
233
+ budget_tokens: int,
234
+ *,
235
+ provider: str = "",
236
+ model: str = "",
237
+ services: PromptCompactionServices,
238
+ ) -> dict[str, Any]:
239
+ """Fit a Responses ``input`` array inside the target model's budget.
240
+
241
+ The Anthropic and chat wires have had this since the context guard was
242
+ written; Responses did not, so a session carried across a model change kept
243
+ a history the new window cannot hold. The upstream then rejects every turn
244
+ with ``context_length_exceeded``, and the client's own recovery drops one
245
+ history item per round trip, which does not converge on a transcript whose
246
+ bulk is a handful of very large tool outputs.
247
+
248
+ Same policy as the other wires: keep the newest items that fit, replace the
249
+ rest with the deterministic chunked summary, and never split a call from
250
+ its output.
251
+ """
252
+
253
+ items = body.get("input")
254
+ if not isinstance(items, list) or not items:
255
+ return body
256
+ typed = [item for item in items if isinstance(item, dict)]
257
+ if len(typed) != len(items):
258
+ return body
259
+ text = services.text
260
+ runtime = services.runtime
261
+ budget_tokens = max(8192, budget_tokens)
262
+ initial_tokens = runtime.estimate_tokens(body)
263
+ if initial_tokens <= budget_tokens:
264
+ return body
265
+
266
+ summary_budget = max(1024, min(24576, budget_tokens // 10))
267
+ tail_budget = max(8192, budget_tokens - summary_budget)
268
+ tail_start = len(typed)
269
+ for index in range(len(typed) - 1, -1, -1):
270
+ candidate = dict(body)
271
+ candidate["input"] = typed[index:]
272
+ if runtime.estimate_tokens(candidate) > tail_budget:
273
+ break
274
+ tail_start = index
275
+ while tail_start < len(typed) and not responses_tail_is_safe(typed, tail_start):
276
+ tail_start += 1
277
+ if tail_start >= len(typed):
278
+ tail_start = len(typed) - 1
279
+ while tail_start > 0 and not responses_tail_is_safe(typed, tail_start):
280
+ tail_start -= 1
281
+
282
+ def projected(omitted: list[dict[str, Any]]) -> dict[str, Any]:
283
+ summary = text.build_summary(
284
+ [responses_item_as_message(item) for item in omitted], budget_tokens
285
+ )
286
+ return {
287
+ "type": "message",
288
+ "role": "user",
289
+ "content": [{"type": "input_text", "text": summary}],
290
+ }
291
+
292
+ tail = typed[tail_start:]
293
+ out = dict(body)
294
+ out["input"] = [projected(typed[:tail_start]), *tail]
295
+ while runtime.estimate_tokens(out) > budget_tokens and len(tail) > 1:
296
+ tail = tail[1:]
297
+ while tail and not responses_tail_is_safe(tail, 0):
298
+ tail = tail[1:]
299
+ if not tail:
300
+ tail = [typed[-1]]
301
+ break
302
+ out["input"] = [projected(typed[: len(typed) - len(tail)]), *tail]
303
+ final_tokens = runtime.estimate_tokens(out)
304
+ runtime.log(
305
+ "WARN",
306
+ f"compacted responses payload provider={provider} model={model} "
307
+ f"items {len(typed)}->{len(out['input'])} tokens {initial_tokens}->{final_tokens} budget={budget_tokens}",
308
+ )
309
+ omitted = typed[: len(typed) - len(tail)]
310
+ chunk_count = text.chunk_count(
311
+ [responses_item_as_message(item) for item in omitted], budget_tokens
312
+ )
313
+ if chunk_count and (provider or model):
314
+ runtime.write_activity(
315
+ provider or "provider",
316
+ model,
317
+ chunks=chunk_count,
318
+ parallel_sessions=1,
319
+ tokens=initial_tokens,
320
+ final_tokens=final_tokens,
321
+ budget=budget_tokens,
322
+ omitted_messages=len(omitted),
323
+ retained_messages=len(out["input"]),
324
+ )
325
+ return out
326
+
327
+
184
328
  def anthropic_message_has_tool_result(message: dict[str, Any]) -> bool:
185
329
  content = message.get("content")
186
330
  return isinstance(content, list) and any(
@@ -174,3 +174,48 @@ def normalize_anthropic_system_role_messages(
174
174
  out["messages"] = next_messages
175
175
  out["system"] = append_anthropic_system_texts(body.get("system"), system_texts)
176
176
  return out
177
+
178
+
179
+ def inline_anthropic_system_role_messages(
180
+ body: Mapping[str, Any],
181
+ content_to_text: Callable[[Any], str],
182
+ ) -> JsonObject:
183
+ """Make non-standard system history wire-valid without moving its position.
184
+
185
+ Anthropic Messages only permits user/assistant roles in ``messages``. A
186
+ late runtime reminder must therefore be represented as user context, but
187
+ moving it into the leading ``system`` field would rewrite the entire cache
188
+ prefix on every turn.
189
+ """
190
+
191
+ messages = body.get("messages")
192
+ if not isinstance(messages, list):
193
+ return dict(body)
194
+ changed = False
195
+ projected: list[Any] = []
196
+ for message in messages:
197
+ if not isinstance(message, dict) or str(message.get("role") or "").strip() != "system":
198
+ projected.append(message)
199
+ continue
200
+ changed = True
201
+ text = content_to_text(message.get("content")).strip()
202
+ projected.append(
203
+ {
204
+ **message,
205
+ "role": "user",
206
+ "content": f"[Runtime system context]\n{text}" if text else "[Runtime system context]",
207
+ }
208
+ )
209
+ if not changed:
210
+ return dict(body)
211
+ return {**body, "messages": projected}
212
+
213
+
214
+ def normalize_anthropic_system_role_messages_by_strategy(
215
+ body: Mapping[str, Any],
216
+ strategy: str,
217
+ content_to_text: Callable[[Any], str],
218
+ ) -> JsonObject:
219
+ if strategy == "hoist_top_level":
220
+ return normalize_anthropic_system_role_messages(body, content_to_text)
221
+ return inline_anthropic_system_role_messages(body, content_to_text)
@@ -210,7 +210,7 @@ class AnthropicThinkingPolicy:
210
210
  self._ports.log(
211
211
  "INFO",
212
212
  "removed top-level Anthropic thinking request but preserved thinking blocks "
213
- f"for OpenAI-chat reasoning passback provider={provider} "
213
+ f"for provider reasoning passback provider={provider} "
214
214
  f"thinking_blocks={block_count}",
215
215
  )
216
216
  return projected