@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,256 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+
8
+ _EXTERNAL_PROVENANCE_META_KEYS = (
9
+ "mcp_server",
10
+ "mcp_method",
11
+ "mcp_json",
12
+ "sse_source",
13
+ "sse_event",
14
+ "sse_json",
15
+ "stream_id",
16
+ "sse_id",
17
+ "cursor",
18
+ "event_id",
19
+ "message_id",
20
+ "source_message_id",
21
+ "rpc_id",
22
+ )
23
+
24
+ _UNIQUE_REFERENCE_META_KEYS = (
25
+ "message_id",
26
+ "source_message_id",
27
+ "assignment_id",
28
+ "poll_id",
29
+ "task_id",
30
+ "job_id",
31
+ "schedule_id",
32
+ "reminder_id",
33
+ )
34
+
35
+ _EVENT_ORDER_META_KEYS = (
36
+ "stream_id",
37
+ "cursor",
38
+ "sse_id",
39
+ "event_id",
40
+ "sequence",
41
+ "seq",
42
+ )
43
+
44
+
45
+ def _metadata(message: dict[str, Any]) -> dict[str, Any]:
46
+ meta = message.get("meta")
47
+ return meta if isinstance(meta, dict) else {}
48
+
49
+
50
+ def string_list(value: Any) -> list[str]:
51
+ if value is None:
52
+ return []
53
+ if isinstance(value, str):
54
+ text = value.strip()
55
+ if not text:
56
+ return []
57
+ if text.startswith("[") and text.endswith("]"):
58
+ try:
59
+ parsed = json.loads(text)
60
+ except (json.JSONDecodeError, TypeError):
61
+ parsed = None
62
+ if isinstance(parsed, list):
63
+ return string_list(parsed)
64
+ if text.lower() in ("all", "*"):
65
+ return ["all"]
66
+ return [text]
67
+ if isinstance(value, (list, tuple, set)):
68
+ out: list[str] = []
69
+ for item in value:
70
+ out.extend(string_list(item))
71
+ return out
72
+ text = str(value).strip()
73
+ return [text] if text else []
74
+
75
+
76
+ def message_meta_sources(message: dict[str, Any]) -> list[dict[str, Any]]:
77
+ meta = _metadata(message)
78
+ sources: list[dict[str, Any]] = []
79
+ if meta:
80
+ sources.append(meta)
81
+ for envelope_key in ("mcp_json", "sse_json"):
82
+ envelope = meta.get(envelope_key)
83
+ if not isinstance(envelope, dict):
84
+ continue
85
+ params = envelope.get("params")
86
+ if isinstance(params, dict):
87
+ nested_meta = params.get("meta")
88
+ if isinstance(nested_meta, dict):
89
+ sources.append(nested_meta)
90
+ nested_meta = envelope.get("meta")
91
+ if isinstance(nested_meta, dict):
92
+ sources.append(nested_meta)
93
+ return sources
94
+
95
+
96
+ def message_delivery_targets(message: dict[str, Any]) -> set[str]:
97
+ return {item.strip().lower() for item in string_list(message.get("delivery")) if item.strip()}
98
+
99
+
100
+ def message_is_web_chat_request(message: dict[str, Any]) -> bool:
101
+ meta = _metadata(message)
102
+ source = str(meta.get("source") or "").strip().lower()
103
+ kind = str(message.get("kind") or meta.get("kind") or "").strip().lower()
104
+ return bool(
105
+ source == "ciel-runtime-web-chat"
106
+ or kind == "web_chat"
107
+ or meta.get("reply_channel")
108
+ or meta.get("reply_recipient")
109
+ )
110
+
111
+
112
+ def message_has_external_provenance(message: dict[str, Any]) -> bool:
113
+ meta = _metadata(message)
114
+ if message_is_web_chat_request(message):
115
+ return True
116
+ return any(
117
+ meta.get(key) is not None and str(meta[key]).strip()
118
+ for key in _EXTERNAL_PROVENANCE_META_KEYS
119
+ )
120
+
121
+
122
+ def message_has_unique_reference(message: dict[str, Any]) -> bool:
123
+ meta_sources = message_meta_sources(message)
124
+ if any(
125
+ meta.get(key) is not None and str(meta[key]).strip()
126
+ for key in _UNIQUE_REFERENCE_META_KEYS
127
+ for meta in meta_sources
128
+ ):
129
+ return True
130
+ for meta in meta_sources:
131
+ message_ids = meta.get("message_ids")
132
+ if isinstance(message_ids, (list, tuple)) and any(str(item).strip() for item in message_ids):
133
+ return True
134
+ if isinstance(message_ids, str) and message_ids.strip():
135
+ return True
136
+ rooms = meta.get("rooms")
137
+ if not isinstance(rooms, str) or not rooms.strip():
138
+ continue
139
+ try:
140
+ parsed_rooms = json.loads(rooms)
141
+ except (json.JSONDecodeError, TypeError):
142
+ continue
143
+ if not isinstance(parsed_rooms, list):
144
+ continue
145
+ for room in parsed_rooms:
146
+ if not isinstance(room, dict):
147
+ continue
148
+ room_message_ids = room.get("message_ids")
149
+ if isinstance(room_message_ids, (list, tuple)) and any(
150
+ str(item).strip() for item in room_message_ids
151
+ ):
152
+ return True
153
+ return False
154
+
155
+
156
+ def message_source_key(message: dict[str, Any]) -> str:
157
+ meta = _metadata(message)
158
+ return str(meta.get("mcp_server") or meta.get("sse_source") or meta.get("source") or "").strip()
159
+
160
+
161
+ def message_kind_key(message: dict[str, Any]) -> str:
162
+ meta = _metadata(message)
163
+ meta_kind = str(
164
+ meta.get("kind")
165
+ or meta.get("type")
166
+ or meta.get("event_type")
167
+ or meta.get("eventType")
168
+ or meta.get("event")
169
+ or ""
170
+ ).strip()
171
+ return meta_kind or str(message.get("kind") or "").strip()
172
+
173
+
174
+ def message_topic_key(message: dict[str, Any]) -> str:
175
+ meta = _metadata(message)
176
+ return str(
177
+ meta.get("key")
178
+ or meta.get("topic")
179
+ or meta.get("resource")
180
+ or meta.get("target")
181
+ or ""
182
+ ).strip()
183
+
184
+
185
+ def message_order_value(message: dict[str, Any]) -> tuple[int, int, int] | None:
186
+ meta = _metadata(message)
187
+ for key in _EVENT_ORDER_META_KEYS:
188
+ raw = meta.get(key)
189
+ if raw is None:
190
+ continue
191
+ text = str(raw).strip()
192
+ if not text:
193
+ continue
194
+ match = re.fullmatch(r"(\d+)-(\d+)", text)
195
+ if match:
196
+ return (2, int(match.group(1)), int(match.group(2)))
197
+ if text.isdigit():
198
+ return (1, int(text), 0)
199
+ if message_has_external_provenance(message):
200
+ try:
201
+ message_id = int(message.get("id") or 0)
202
+ except (TypeError, ValueError):
203
+ message_id = 0
204
+ if message_id > 0:
205
+ return (3, message_id, 0)
206
+ return None
207
+
208
+
209
+ def message_coalesce_key(message: dict[str, Any]) -> tuple[str, str, str, str, str] | None:
210
+ if message_delivery_targets(message) and not message_has_external_provenance(message):
211
+ return None
212
+ if message_is_web_chat_request(message) or message_has_unique_reference(message):
213
+ return None
214
+ source = message_source_key(message)
215
+ if not source or message_order_value(message) is None:
216
+ return None
217
+ meta = _metadata(message)
218
+ method = str(meta.get("mcp_method") or meta.get("sse_event") or "").strip()
219
+ kind = message_kind_key(message)
220
+ if not method and not kind:
221
+ return None
222
+ channel = str(
223
+ message.get("channel")
224
+ or meta.get("room_id")
225
+ or meta.get("room")
226
+ or meta.get("channel")
227
+ or ""
228
+ ).strip()
229
+ return (source, channel, method, kind, message_topic_key(message))
230
+
231
+
232
+ def superseded_message_ids(messages: list[dict[str, Any]]) -> set[int]:
233
+ latest: dict[tuple[str, str, str, str, str], tuple[tuple[int, int, int], int]] = {}
234
+ superseded: set[int] = set()
235
+ for message in messages:
236
+ try:
237
+ message_id = int(message.get("id") or 0)
238
+ except (TypeError, ValueError):
239
+ continue
240
+ if message_id <= 0:
241
+ continue
242
+ key = message_coalesce_key(message)
243
+ order = message_order_value(message) if key is not None else None
244
+ if key is None or order is None:
245
+ continue
246
+ previous = latest.get(key)
247
+ if previous is None:
248
+ latest[key] = (order, message_id)
249
+ continue
250
+ previous_order, previous_id = previous
251
+ if (order, message_id) >= (previous_order, previous_id):
252
+ superseded.add(previous_id)
253
+ latest[key] = (order, message_id)
254
+ else:
255
+ superseded.add(message_id)
256
+ return superseded
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from ciel_runtime_support.channel_event_projection import (
8
+ CHANNEL_CONTROL_KINDS,
9
+ metadata_key_is_sensitive,
10
+ pretty_json_value,
11
+ )
12
+ from ciel_runtime_support.channel_message_policy import (
13
+ message_has_external_provenance,
14
+ message_is_web_chat_request,
15
+ message_meta_sources,
16
+ string_list,
17
+ )
18
+
19
+
20
+ NATIVE_ROUTER_CHANNEL_NAMES = frozenset({"ciel-runtime-router", "mcp-ciel-runtime-router"})
21
+
22
+ _PROMPT_META_KEYS = (
23
+ "kind",
24
+ "type",
25
+ "event_type",
26
+ "eventType",
27
+ "status",
28
+ "mcp_server",
29
+ "mcp_method",
30
+ "room_name",
31
+ "room_label",
32
+ "room_id",
33
+ "room",
34
+ "channel",
35
+ "thread_id",
36
+ "parent_id",
37
+ "message_id",
38
+ "source_message_id",
39
+ "event_id",
40
+ "stream_id",
41
+ "sse_id",
42
+ "cursor",
43
+ "sequence",
44
+ "seq",
45
+ "assignment_id",
46
+ "poll_id",
47
+ "task_id",
48
+ "round_id",
49
+ "conversation_id",
50
+ "session_id",
51
+ "agent_id",
52
+ "agent_name",
53
+ "sender_id",
54
+ "sender",
55
+ "sender_name",
56
+ "author_id",
57
+ "author_name",
58
+ "recipient_id",
59
+ "recipient",
60
+ "recipient_name",
61
+ "mentioned_by",
62
+ "key",
63
+ "path",
64
+ )
65
+
66
+
67
+ def _metadata(message: dict[str, Any]) -> dict[str, Any]:
68
+ meta = message.get("meta")
69
+ return meta if isinstance(meta, dict) else {}
70
+
71
+
72
+ def prompt_scalar(value: Any) -> Any:
73
+ if value is None or isinstance(value, (bool, int, float)):
74
+ return value
75
+ if isinstance(value, str):
76
+ text = value.strip()
77
+ return text if text and len(text) <= 240 else None
78
+ if isinstance(value, list):
79
+ out = [scalar for item in value[:10] if (scalar := prompt_scalar(item)) is not None]
80
+ if not out:
81
+ return None
82
+ try:
83
+ encoded = json.dumps(out, ensure_ascii=False, separators=(",", ":"), default=str)
84
+ except (TypeError, ValueError, OverflowError):
85
+ return None
86
+ return out if len(encoded) <= 300 else None
87
+ return None
88
+
89
+
90
+ def prompt_metadata(message: dict[str, Any]) -> str:
91
+ meta = _metadata(message)
92
+ if not meta:
93
+ return ""
94
+ values = {
95
+ key: value
96
+ for key in _PROMPT_META_KEYS
97
+ if key in meta
98
+ and not metadata_key_is_sensitive(key)
99
+ and (value := prompt_scalar(meta.get(key))) is not None
100
+ }
101
+ kept: dict[str, Any] = {}
102
+ for key, value in values.items():
103
+ candidate = {**kept, key: value}
104
+ text = json.dumps(candidate, ensure_ascii=False, separators=(",", ":"), default=str)
105
+ if len(text) <= 900:
106
+ kept = candidate
107
+ return json.dumps(kept, ensure_ascii=False, separators=(",", ":"), default=str) if kept else ""
108
+
109
+
110
+ def format_wake_prompt(message: dict[str, Any]) -> str:
111
+ channel = str(message.get("channel") or "default")
112
+ sender = str(message.get("sender_id") or "channel")
113
+ message_id = str(message.get("id") or "")
114
+ meta = _metadata(message)
115
+ room = str(meta.get("room_id") or meta.get("room") or channel)
116
+ thread = str(message.get("thread_id") or meta.get("thread_id") or "")
117
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip()
118
+ fields = [f"channel={channel}", f"room={room}", f"from={sender}"]
119
+ if message_id:
120
+ fields.append(f"id={message_id}")
121
+ if thread:
122
+ fields.append(f"thread={thread}")
123
+ metadata = prompt_metadata(message)
124
+ if metadata:
125
+ fields.append(f"metadata={metadata}")
126
+ return (
127
+ "[ciel-runtime external channel message] "
128
+ + " ".join(fields)
129
+ + f" text={json.dumps(body, ensure_ascii=False)}"
130
+ )
131
+
132
+
133
+ def _format_web_chat_wake_item(message: dict[str, Any]) -> str:
134
+ channel = str(message.get("channel") or "default")
135
+ meta = _metadata(message)
136
+ reply_channel = str(meta.get("reply_channel") or channel)
137
+ thread = str(message.get("thread_id") or meta.get("thread_id") or reply_channel)
138
+ message_id = str(message.get("id") or "")
139
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip()
140
+ fields = [f"id={message_id}", f"channel={reply_channel}", f"thread={thread}"]
141
+ return " ".join(field for field in fields if not field.endswith("=")) + (
142
+ f" user={json.dumps(body, ensure_ascii=False)}"
143
+ )
144
+
145
+
146
+ def _web_chat_reply_routes(messages: list[dict[str, Any]]) -> list[dict[str, str]]:
147
+ routes: list[dict[str, str]] = []
148
+ seen: set[tuple[str, str]] = set()
149
+ for message in messages:
150
+ if not message_is_web_chat_request(message):
151
+ continue
152
+ meta = _metadata(message)
153
+ channel = str(meta.get("reply_channel") or message.get("channel") or "default").strip()
154
+ thread = str(message.get("thread_id") or meta.get("thread_id") or channel).strip()
155
+ route = (channel, thread)
156
+ if route in seen:
157
+ continue
158
+ seen.add(route)
159
+ routes.append({"channel": channel, "thread_id": thread})
160
+ return routes
161
+
162
+
163
+ def _web_chat_reply_instruction(messages: list[dict[str, Any]]) -> str:
164
+ routes = _web_chat_reply_routes(messages)
165
+ if not routes:
166
+ return ""
167
+ encoded_routes = json.dumps(routes, ensure_ascii=False, separators=(",", ":"))
168
+ return (
169
+ "[ciel-runtime web reply required] Do not leave the answer only in the terminal. "
170
+ "After completing the answer, call MCP server `ciel-runtime-router` tool "
171
+ "`send_message` once for each route in "
172
+ f"{encoded_routes}, with that channel and thread_id, recipients=[\"web\"], "
173
+ "delivery=[\"web\"], kind=\"reply\", and message set to the complete answer. "
174
+ "Use `send_file` on the same route for files."
175
+ )
176
+
177
+
178
+ def format_web_chat_wake_batch_prompt(messages: list[dict[str, Any]]) -> str:
179
+ items = " ; ".join(_format_web_chat_wake_item(message) for message in messages)
180
+ prompt = f"[ciel-runtime web chat] {len(messages)} browser message(s): {items}"
181
+ instruction = _web_chat_reply_instruction(messages)
182
+ return f"{prompt}\n\n{instruction}" if instruction else prompt
183
+
184
+
185
+ def wake_message_noise_reason(message: dict[str, Any]) -> str | None:
186
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip().lower()
187
+ kind = str(message.get("kind") or "").strip().lower()
188
+ if not body:
189
+ return "empty"
190
+ if kind in {"connection", "connected", "heartbeat", "keepalive"}:
191
+ return kind
192
+ if re.fullmatch(r"[a-z0-9_.:-]{1,80}\.(ws|sse)\.connected", body):
193
+ return "transport_connected"
194
+ return None
195
+
196
+
197
+ def llm_message_skip_reason(message: dict[str, Any]) -> str | None:
198
+ visibility = str(message.get("visibility") or "user").strip().lower()
199
+ if visibility in {"hidden", "internal", "transport", "control", "system"}:
200
+ return f"visibility_{visibility}"
201
+ recipients = {item.strip().lower() for item in string_list(message.get("recipients"))}
202
+ if "internal" in recipients:
203
+ return "recipient_internal"
204
+ delivery = string_list(message.get("delivery"))
205
+ if delivery and not ({"all", "*", "llm"} & {item.strip().lower() for item in delivery}):
206
+ return "delivery_not_llm"
207
+ noise_reason = wake_message_noise_reason(message)
208
+ if noise_reason:
209
+ return noise_reason
210
+ meta = _metadata(message)
211
+ source = str(meta.get("sse_source") or meta.get("source") or "").strip().lower()
212
+ sender = str(message.get("sender_id") or meta.get("sender_id") or "").strip().lower()
213
+ if source in NATIVE_ROUTER_CHANNEL_NAMES or sender in NATIVE_ROUTER_CHANNEL_NAMES:
214
+ return "native_router_self_echo"
215
+ meta_kind = str(
216
+ meta.get("kind")
217
+ or meta.get("type")
218
+ or meta.get("event_type")
219
+ or meta.get("eventType")
220
+ or meta.get("event")
221
+ or meta.get("status")
222
+ or ""
223
+ ).strip().lower()
224
+ if meta_kind in CHANNEL_CONTROL_KINDS:
225
+ return meta_kind
226
+ if not delivery and not message_has_external_provenance(message):
227
+ return "unscoped_channel_message"
228
+ return None
229
+
230
+
231
+ def wake_message_is_noise(message: dict[str, Any]) -> bool:
232
+ return wake_message_noise_reason(message) is not None
233
+
234
+
235
+ def format_wake_batch_prompt(messages: list[dict[str, Any]]) -> str:
236
+ if len(messages) == 1:
237
+ return format_wake_prompt(messages[0])
238
+ parts: list[str] = []
239
+ for message in messages:
240
+ channel = str(message.get("channel") or "default")
241
+ sender = str(message.get("sender_id") or "channel")
242
+ message_id = str(message.get("id") or "")
243
+ meta = _metadata(message)
244
+ room = str(meta.get("room_id") or meta.get("room") or channel)
245
+ thread = str(message.get("thread_id") or meta.get("thread_id") or "")
246
+ body = str(message.get("message") or "")
247
+ fields = [f"id={message_id}", f"room={room}", f"from={sender}"]
248
+ if thread:
249
+ fields.append(f"thread={thread}")
250
+ metadata = prompt_metadata(message)
251
+ if metadata:
252
+ fields.append(f"metadata={metadata}")
253
+ parts.append("(" + " ".join(fields) + ") " + json.dumps(body, ensure_ascii=False))
254
+ return f"[ciel-runtime external channel messages] {len(messages)} new messages: " + " ; ".join(parts)
255
+
256
+
257
+ def _source_header_scalar(value: Any) -> str:
258
+ if value is None or isinstance(value, (dict, list)):
259
+ return ""
260
+ text = re.sub(r"\s+", " ", str(value)).strip()
261
+ return text if text and len(text) <= 240 else ""
262
+
263
+
264
+ def _first_source_header_value(message: dict[str, Any], keys: tuple[str, ...]) -> str:
265
+ for source in message_meta_sources(message):
266
+ for key in keys:
267
+ if metadata_key_is_sensitive(key):
268
+ continue
269
+ text = _source_header_scalar(source.get(key))
270
+ if text:
271
+ return text
272
+ return ""
273
+
274
+
275
+ def _message_source_header(message: dict[str, Any]) -> str:
276
+ meta = _metadata(message)
277
+ if not any(isinstance(meta.get(key), (dict, list)) for key in ("mcp_json", "sse_json")):
278
+ return ""
279
+ room_name = _first_source_header_value(message, ("room_name", "room_label", "title", "name"))
280
+ room_id = _first_source_header_value(message, ("room_id", "room"))
281
+ channel = _first_source_header_value(message, ("channel",)) or _source_header_scalar(
282
+ message.get("channel")
283
+ )
284
+ source = room_name or room_id or channel
285
+ if not source:
286
+ return ""
287
+ source_text = f"{room_name} (room_id={room_id})" if room_name and room_id != room_name else source
288
+ return f"[Source channel] {source_text}"
289
+
290
+
291
+ def message_llm_display_text(message: dict[str, Any]) -> str:
292
+ meta = _metadata(message)
293
+ for key in ("mcp_json", "sse_json"):
294
+ value = meta.get(key)
295
+ if isinstance(value, (dict, list)):
296
+ text = pretty_json_value(value)
297
+ header = _message_source_header(message)
298
+ return f"{header}\n\n{text}" if header else text
299
+ return str(message.get("message") if message.get("message") is not None else "")
300
+
301
+
302
+ def format_llm_batch_prompt(messages: list[dict[str, Any]]) -> str:
303
+ prompt = "\n\n".join(message_llm_display_text(message) for message in messages)
304
+ instruction = _web_chat_reply_instruction(messages)
305
+ return f"{prompt}\n\n{instruction}" if instruction else prompt