@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,253 @@
1
+ """Parse local router control requests embedded in runtime conversations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+ from dataclasses import dataclass
8
+ from typing import Any, Callable
9
+
10
+
11
+ PLACEHOLDER_VALUES = frozenset(
12
+ {"", "$0", "${0}", "$1", "${1}", "$2", "${2}", "$ARGUMENTS", "${ARGUMENTS}"}
13
+ )
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ShortcutTextServices:
18
+ latest_user_text: Callable[[dict[str, Any]], str]
19
+
20
+
21
+ def has_marker(
22
+ body: dict[str, Any], markers: tuple[str, ...], services: ShortcutTextServices
23
+ ) -> bool:
24
+ text = services.latest_user_text(body)
25
+ return any(marker in text for marker in markers)
26
+
27
+
28
+ def marker_tail(
29
+ body: dict[str, Any], markers: tuple[str, ...], services: ShortcutTextServices
30
+ ) -> str:
31
+ text = services.latest_user_text(body)
32
+ for marker in markers:
33
+ if marker in text:
34
+ return text.split(marker, 1)[1]
35
+ return ""
36
+
37
+
38
+ def single_value(
39
+ body: dict[str, Any],
40
+ markers: tuple[str, ...],
41
+ services: ShortcutTextServices,
42
+ *,
43
+ empty_default: str,
44
+ blank_value_default: str,
45
+ ) -> str:
46
+ tail = marker_tail(body, markers, services)
47
+ if not tail:
48
+ return empty_default
49
+ for line in tail.splitlines():
50
+ stripped = line.strip()
51
+ if stripped.lower().startswith("value:"):
52
+ return stripped.split(":", 1)[1].strip() or blank_value_default
53
+ return tail.strip() or blank_value_default
54
+
55
+
56
+ def live_option_value(
57
+ body: dict[str, Any], markers: tuple[str, ...], services: ShortcutTextServices
58
+ ) -> str:
59
+ tail = marker_tail(body, markers, services)
60
+ if not tail:
61
+ return "status"
62
+ fallback_values: list[str] = []
63
+ structured = False
64
+ for line in tail.splitlines():
65
+ stripped = line.strip()
66
+ lowered = stripped.lower()
67
+ if lowered.startswith("value:"):
68
+ structured = True
69
+ value = stripped.split(":", 1)[1].strip()
70
+ if value not in PLACEHOLDER_VALUES:
71
+ return value
72
+ elif lowered.startswith("arguments:"):
73
+ structured = True
74
+ value = stripped.split(":", 1)[1].strip()
75
+ if value not in PLACEHOLDER_VALUES:
76
+ fallback_values.append(value)
77
+ if fallback_values:
78
+ return fallback_values[0]
79
+ if structured:
80
+ return "status"
81
+ fallback = tail.strip()
82
+ return fallback if fallback not in PLACEHOLDER_VALUES else "status"
83
+
84
+
85
+ def live_api_keys_value(
86
+ body: dict[str, Any], markers: tuple[str, ...], services: ShortcutTextServices
87
+ ) -> str:
88
+ tail = marker_tail(body, markers, services)
89
+ if not tail:
90
+ return "status"
91
+ value_line = ""
92
+ arguments: list[str] = []
93
+ structured = False
94
+ capture_arguments = False
95
+ for line in tail.splitlines():
96
+ stripped = line.strip()
97
+ if capture_arguments:
98
+ if stripped not in PLACEHOLDER_VALUES:
99
+ arguments.append(stripped)
100
+ continue
101
+ lowered = stripped.lower()
102
+ if lowered.startswith("value:"):
103
+ structured = True
104
+ value = stripped.split(":", 1)[1].strip()
105
+ if value not in PLACEHOLDER_VALUES:
106
+ value_line = value
107
+ elif lowered.startswith("arguments:"):
108
+ structured = True
109
+ capture_arguments = True
110
+ value = stripped.split(":", 1)[1].strip()
111
+ if value not in PLACEHOLDER_VALUES:
112
+ arguments.append(value)
113
+ if arguments:
114
+ return "\n".join(arguments)
115
+ if value_line:
116
+ return value_line
117
+ if structured:
118
+ return "status"
119
+ fallback = tail.strip()
120
+ return fallback if fallback not in PLACEHOLDER_VALUES else "status"
121
+
122
+
123
+ def parse_channel_bridge_args(raw: str) -> tuple[str, dict[str, str]]:
124
+ text = str(raw or "").strip()
125
+ if not text:
126
+ return "status", {}
127
+ try:
128
+ parts = shlex.split(text)
129
+ except Exception:
130
+ parts = text.split()
131
+ if not parts:
132
+ return "status", {}
133
+ command = parts[0].strip().lower()
134
+ if command not in {"status", "poll", "wait", "send", "post", "sse"}:
135
+ parts = ["status", *parts]
136
+ command = "status"
137
+ options: dict[str, str] = {}
138
+ loose: list[str] = []
139
+ for part in parts[1:]:
140
+ if "=" in part:
141
+ key, value = part.split("=", 1)
142
+ options[key.strip().lower().replace("-", "_")] = value.strip()
143
+ elif part:
144
+ loose.append(part)
145
+ if loose and "message" not in options and command in {"send", "post"}:
146
+ options["message"] = " ".join(loose)
147
+ return command, options
148
+
149
+
150
+ def format_channel_messages(messages: list[dict[str, Any]], after: int) -> str:
151
+ if not messages:
152
+ return f"No channel messages after id {after}."
153
+ lines = [f"Channel bridge messages ({len(messages)}):"]
154
+ for item in messages:
155
+ recipients = item.get("recipients") or []
156
+ recipient_text = (
157
+ ",".join(str(value) for value in recipients) or "all"
158
+ if isinstance(recipients, list)
159
+ else str(recipients or "all")
160
+ )
161
+ text = re.sub(r"\s+", " ", str(item.get("message") or "")).strip()
162
+ if len(text) > 500:
163
+ text = text[:500].rstrip() + "..."
164
+ lines.append(
165
+ f"- #{item.get('id')} [{item.get('channel')}] {item.get('sender_id')} -> {recipient_text}: {text}"
166
+ )
167
+ lines.append(f"Last id: {messages[-1].get('id')}")
168
+ return "\n".join(lines)
169
+
170
+
171
+ def _strip_wrapping_quotes(value: str) -> str:
172
+ text = str(value or "").strip()
173
+ if len(text) >= 2 and text[0] == text[-1] and text[0] in {"'", '"'}:
174
+ return text[1:-1]
175
+ return text
176
+
177
+
178
+ def split_import_session_arguments(value: str, *, posix: bool) -> tuple[str, str]:
179
+ raw = str(value or "").strip()
180
+ if not raw:
181
+ return "", ""
182
+ try:
183
+ parts = [_strip_wrapping_quotes(part) for part in shlex.split(raw, posix=posix)]
184
+ except Exception:
185
+ parts = raw.split()
186
+ if not parts:
187
+ return "", ""
188
+ target = parts[0].strip()
189
+ path_parts: list[str] = []
190
+ index = 1
191
+ while index < len(parts):
192
+ part = str(parts[index]).strip()
193
+ lowered = part.lower()
194
+ if lowered in {"--path", "-p", "path", "file", "transcript"} and index + 1 < len(parts):
195
+ path_parts = [str(parts[index + 1])]
196
+ index += 2
197
+ continue
198
+ if lowered.startswith(("--path=", "path=", "file=", "transcript=")):
199
+ path_parts = [part.split("=", 1)[1]]
200
+ else:
201
+ path_parts.append(part)
202
+ index += 1
203
+ return target, _strip_wrapping_quotes(" ".join(path_parts).strip())
204
+
205
+
206
+ def import_session_args(
207
+ body: dict[str, Any],
208
+ markers: tuple[str, ...],
209
+ services: ShortcutTextServices,
210
+ *,
211
+ posix: bool,
212
+ ) -> tuple[str, str]:
213
+ tail = marker_tail(body, markers, services)
214
+ if not tail:
215
+ return "", ""
216
+ target = ""
217
+ path = ""
218
+ arguments: list[str] = []
219
+ capture = False
220
+ field_prefixes = ("target:", "source:", "format:", "runtime:")
221
+ path_prefixes = ("path:", "file:", "transcript:")
222
+ for line in tail.splitlines():
223
+ stripped = line.strip()
224
+ if capture:
225
+ if stripped not in PLACEHOLDER_VALUES:
226
+ arguments.append(stripped)
227
+ continue
228
+ lowered = stripped.lower()
229
+ if lowered.startswith(field_prefixes):
230
+ value = stripped.split(":", 1)[1].strip()
231
+ if value not in PLACEHOLDER_VALUES:
232
+ target = value
233
+ elif lowered.startswith(path_prefixes):
234
+ value = stripped.split(":", 1)[1].strip()
235
+ if value not in PLACEHOLDER_VALUES:
236
+ path = _strip_wrapping_quotes(value)
237
+ elif lowered.startswith("arguments:"):
238
+ capture = True
239
+ value = stripped.split(":", 1)[1].strip()
240
+ if value not in PLACEHOLDER_VALUES:
241
+ arguments.append(value)
242
+ arg_target, arg_path = split_import_session_arguments("\n".join(arguments), posix=posix)
243
+ target = arg_target or target
244
+ path = arg_path or path
245
+ if not target:
246
+ fallback = "\n".join(
247
+ line
248
+ for line in tail.splitlines()
249
+ if not line.strip().lower().startswith((*field_prefixes, *path_prefixes, "arguments:"))
250
+ ).strip()
251
+ target, fallback_path = split_import_session_arguments(fallback, posix=posix)
252
+ path = path or fallback_path
253
+ return target.strip(), _strip_wrapping_quotes(path)
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import time
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ from .usage_events import UsageEvent
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RequestTracePolicy:
16
+ enabled: Callable[[], bool]
17
+ request_path: Path
18
+ response_path: Path
19
+ request_max_bytes: int
20
+ response_max_bytes: int
21
+ response_text_limit: int
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class RequestTraceProjection:
26
+ content_to_text: Callable[[Any], str]
27
+ thinking_block_count: Callable[[dict[str, Any]], int]
28
+ tool_continuation_block_count: Callable[[dict[str, Any]], int]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class RequestTraceServices:
33
+ policy: RequestTracePolicy
34
+ projection: RequestTraceProjection
35
+ log: Callable[[str, str], None]
36
+ timestamp: Callable[[], str] = lambda: time.strftime("%Y-%m-%dT%H:%M:%S")
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ResponseTraceController:
41
+ record_usage: Callable[[UsageEvent], None]
42
+ publish_event: Callable[..., Any]
43
+ services: Callable[[], RequestTraceServices]
44
+ log: Callable[[str, str], None]
45
+
46
+ def write(
47
+ self,
48
+ provider: str,
49
+ model: str,
50
+ text: str,
51
+ tool_calls: list[dict[str, Any]],
52
+ stop_reason: str | None,
53
+ input_tokens: int,
54
+ output_tokens: int,
55
+ last_chunk: dict[str, Any] | None = None,
56
+ ) -> None:
57
+ try:
58
+ self.record_usage(
59
+ UsageEvent(
60
+ provider=provider,
61
+ model=model,
62
+ input_tokens=input_tokens,
63
+ output_tokens=output_tokens,
64
+ )
65
+ )
66
+ self.publish_event(
67
+ level="info",
68
+ category="usage.response",
69
+ message="upstream token usage recorded",
70
+ provider=provider,
71
+ model=model,
72
+ data={
73
+ "input_tokens": max(0, input_tokens),
74
+ "output_tokens": max(0, output_tokens),
75
+ },
76
+ )
77
+ except Exception as exc:
78
+ self.log(
79
+ "WARN",
80
+ "usage_event_record_failed "
81
+ f"error={type(exc).__name__}: {exc}",
82
+ )
83
+ dump_response_for_trace(
84
+ provider,
85
+ model,
86
+ text,
87
+ tool_calls,
88
+ stop_reason,
89
+ input_tokens,
90
+ output_tokens,
91
+ self.services(),
92
+ last_chunk=last_chunk,
93
+ )
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class RouterMessagePreviewPolicy:
98
+ environ: Mapping[str, str]
99
+ load_config: Callable[[], dict[str, Any]]
100
+ positive_int: Callable[[Any], int | None]
101
+ latest_user_text: Callable[[dict[str, Any]], str]
102
+ redact_text: Callable[[str], str]
103
+
104
+ def configured_chars(
105
+ self,
106
+ config: dict[str, Any] | None = None,
107
+ ) -> int:
108
+ current = config or self.load_config()
109
+ environment = self.environ.get(
110
+ "CIEL_RUNTIME_ROUTER_MESSAGE_PREVIEW_CHARS",
111
+ "",
112
+ ).strip()
113
+ value = self.positive_int(environment) if environment else None
114
+ if value is None:
115
+ value = self.positive_int(
116
+ current.get("router_debug_message_preview_chars")
117
+ )
118
+ return min(value or 0, 4_000)
119
+
120
+ def project(
121
+ self,
122
+ body: dict[str, Any],
123
+ config: dict[str, Any] | None = None,
124
+ ) -> dict[str, Any]:
125
+ limit = self.configured_chars(config)
126
+ if limit <= 0:
127
+ return {}
128
+ text = self.latest_user_text(body).strip()
129
+ if not text:
130
+ return {
131
+ "message_preview_chars": limit,
132
+ "message_preview": "",
133
+ "message_preview_truncated": False,
134
+ }
135
+ normalized = re.sub(
136
+ r"\s+",
137
+ " ",
138
+ self.redact_text(text),
139
+ )
140
+ return {
141
+ "message_preview_chars": limit,
142
+ "message_preview": normalized[:limit].rstrip(),
143
+ "message_preview_truncated": len(normalized) > limit,
144
+ }
145
+
146
+
147
+ def truncate_for_dump(value: Any, max_len: int = 4000) -> Any:
148
+ try:
149
+ text = json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value
150
+ except Exception:
151
+ text = str(value)
152
+ if len(text) > max_len:
153
+ return text[:max_len] + f"...<truncated {len(text) - max_len} chars>"
154
+ return value
155
+
156
+
157
+ def summarize_messages_for_trace(
158
+ messages: Any,
159
+ projection: RequestTraceProjection,
160
+ max_messages: int = 30,
161
+ ) -> list[dict[str, Any]]:
162
+ if not isinstance(messages, list):
163
+ return []
164
+ selected = messages[-max_messages:]
165
+ offset = len(messages) - len(selected)
166
+ summary: list[dict[str, Any]] = []
167
+ for index, message in enumerate(selected, start=offset):
168
+ if not isinstance(message, dict):
169
+ continue
170
+ entry: dict[str, Any] = {"index": index, "role": message.get("role")}
171
+ blocks: list[dict[str, Any]] = []
172
+ content = message.get("content")
173
+ if isinstance(content, str):
174
+ blocks.append({"type": "text", "text": truncate_for_dump(content, 1000)})
175
+ elif isinstance(content, list):
176
+ for block in content:
177
+ projected = _project_trace_block(block, projection)
178
+ if projected is not None:
179
+ blocks.append(projected)
180
+ else:
181
+ blocks.append(
182
+ {"type": type(content).__name__, "text": truncate_for_dump(content, 1000)}
183
+ )
184
+ entry["content"] = blocks
185
+ summary.append(entry)
186
+ return summary
187
+
188
+
189
+ def _project_trace_block(
190
+ block: Any,
191
+ projection: RequestTraceProjection,
192
+ ) -> dict[str, Any] | None:
193
+ if isinstance(block, str):
194
+ return {"type": "text", "text": truncate_for_dump(block, 1000)}
195
+ if not isinstance(block, dict):
196
+ return None
197
+ block_type = str(block.get("type") or "")
198
+ if block_type == "text":
199
+ return {"type": "text", "text": truncate_for_dump(block.get("text", ""), 1000)}
200
+ if block_type == "tool_use":
201
+ return {
202
+ "type": "tool_use",
203
+ "id": block.get("id"),
204
+ "name": block.get("name"),
205
+ "input": truncate_for_dump(block.get("input"), 1200),
206
+ }
207
+ if block_type == "tool_result":
208
+ return {
209
+ "type": "tool_result",
210
+ "tool_use_id": block.get("tool_use_id"),
211
+ "is_error": block.get("is_error"),
212
+ "content": truncate_for_dump(
213
+ projection.content_to_text(block.get("content", "")),
214
+ 1200,
215
+ ),
216
+ }
217
+ if block_type == "thinking":
218
+ return {
219
+ "type": "thinking",
220
+ "thinking_len": len(str(block.get("thinking") or "")),
221
+ "has_signature": bool(block.get("signature")),
222
+ }
223
+ if block_type == "redacted_thinking":
224
+ return {
225
+ "type": "redacted_thinking",
226
+ "data_len": len(str(block.get("data") or "")),
227
+ }
228
+ return {"type": block_type or "unknown"}
229
+
230
+
231
+ def dump_request_for_trace(
232
+ provider: str,
233
+ path: str,
234
+ body: dict[str, Any],
235
+ services: RequestTraceServices,
236
+ ) -> None:
237
+ if not services.policy.enabled():
238
+ return
239
+ try:
240
+ _rotate_if_oversized(
241
+ services.policy.request_path,
242
+ services.policy.request_max_bytes,
243
+ )
244
+ record = {
245
+ "time": services.timestamp(),
246
+ "provider": provider,
247
+ "path": path,
248
+ "model": body.get("model"),
249
+ "stream": body.get("stream"),
250
+ "thinking": body.get("thinking"),
251
+ "thinking_blocks": services.projection.thinking_block_count(body),
252
+ "tool_continuation_blocks": services.projection.tool_continuation_block_count(body),
253
+ "messages_count": len(body.get("messages") or []),
254
+ "messages": summarize_messages_for_trace(body.get("messages"), services.projection),
255
+ "system": truncate_for_dump(body.get("system")),
256
+ "tools": body.get("tools"),
257
+ }
258
+ _append_json_line(services.policy.request_path, record)
259
+ except Exception as exc:
260
+ services.log("WARN", f"request_trace_dump_failed error={type(exc).__name__}: {exc}")
261
+
262
+
263
+ def dump_response_for_trace(
264
+ provider: str,
265
+ model: str,
266
+ text_so_far: str,
267
+ tool_calls: list[dict[str, Any]],
268
+ stop_reason: str | None,
269
+ input_tokens: int,
270
+ output_tokens: int,
271
+ services: RequestTraceServices,
272
+ last_chunk: dict[str, Any] | None = None,
273
+ ) -> None:
274
+ if not services.policy.enabled():
275
+ return
276
+ try:
277
+ _rotate_if_oversized(
278
+ services.policy.response_path,
279
+ services.policy.response_max_bytes,
280
+ )
281
+ text_full_len = len(text_so_far)
282
+ text = text_so_far
283
+ if text_full_len > services.policy.response_text_limit:
284
+ text = (
285
+ text_so_far[: services.policy.response_text_limit]
286
+ + f"...<truncated {text_full_len - services.policy.response_text_limit} chars>"
287
+ )
288
+ tool_summary = []
289
+ for call in tool_calls:
290
+ function = call.get("function") if isinstance(call.get("function"), dict) else {}
291
+ tool_summary.append(
292
+ {
293
+ "name": function.get("name"),
294
+ "arguments": function.get("arguments"),
295
+ }
296
+ )
297
+ record = {
298
+ "time": services.timestamp(),
299
+ "provider": provider,
300
+ "model": model,
301
+ "stop_reason": stop_reason,
302
+ "input_tokens": input_tokens,
303
+ "output_tokens": output_tokens,
304
+ "text_full_len": text_full_len,
305
+ "tool_call_count": len(tool_calls),
306
+ "text": text,
307
+ "tool_calls": tool_summary,
308
+ "done_reason": (last_chunk or {}).get("done_reason"),
309
+ }
310
+ _append_json_line(services.policy.response_path, record)
311
+ except Exception as exc:
312
+ services.log("WARN", f"response_trace_dump_failed error={type(exc).__name__}: {exc}")
313
+
314
+
315
+ def _rotate_if_oversized(path: Path, max_bytes: int) -> None:
316
+ path.parent.mkdir(parents=True, exist_ok=True)
317
+ if path.exists() and path.stat().st_size > max_bytes:
318
+ path.replace(path.with_suffix(".jsonl.1"))
319
+
320
+
321
+ def _append_json_line(path: Path, record: dict[str, Any]) -> None:
322
+ with path.open("a", encoding="utf-8") as stream:
323
+ stream.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n")