@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/ciel_runtime.py +2553 -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/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -0,0 +1,391 @@
1
+ """Single-response collection bounded context for provider protocols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from http.server import BaseHTTPRequestHandler
8
+ from typing import Any, Callable
9
+
10
+ from .runaway_output_guard import (
11
+ RunawayOutputPolicy,
12
+ policy_from_env,
13
+ trim_runaway_message_content,
14
+ )
15
+ from .ollama_thinking import INTERNAL_REASONING_EFFORT_KEY
16
+ from .ollama_stream_collection import collect_ollama_chat_stream
17
+ from .sse_stream_collection import (
18
+ collect_anthropic_message_stream,
19
+ collect_openai_chat_stream,
20
+ )
21
+ from .response_collection import (
22
+ AnthropicCollectionServices,
23
+ ChatCollectionStrategy,
24
+ ResponseCollectionServices,
25
+ collect_anthropic_message_for_responses,
26
+ collect_chat_message_for_responses,
27
+ )
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ResponseCollectionStrategyPorts:
32
+ ollama_request: Callable[..., dict[str, Any]]
33
+ ollama_decode: Callable[..., dict[str, Any]]
34
+ ollama_timeout: Callable[..., float]
35
+ openai_request: Callable[..., dict[str, Any]]
36
+ openai_decode: Callable[..., dict[str, Any]]
37
+ openai_timeout: Callable[..., float]
38
+ upstream_model: Callable[..., str]
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ResponseCollectionRoutingPorts:
43
+ resolve_model: Callable[..., str]
44
+ select_protocol: Callable[..., str]
45
+ provider_labels: dict[str, str]
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ResponseCollectionStreamPorts:
50
+ """Read collected responses as a stream so a loop is cut while generated."""
51
+
52
+ open_stream: Callable[..., Any] | None = None
53
+ log: Callable[..., None] = lambda _level, _message: None
54
+ policy: Callable[[], RunawayOutputPolicy] = lambda: policy_from_env(os.environ.get)
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class ResponseCollectionContext:
59
+ shared: ResponseCollectionServices
60
+ anthropic: AnthropicCollectionServices
61
+ strategies: ResponseCollectionStrategyPorts
62
+ routing: ResponseCollectionRoutingPorts
63
+ stream: ResponseCollectionStreamPorts = ResponseCollectionStreamPorts()
64
+
65
+ def streaming_collection_enabled(self) -> bool:
66
+ """Whether collected responses are read as a stream.
67
+
68
+ The escape hatch is separate from the guard's own kill switch: an
69
+ operator may want detection kept while reverting the transport.
70
+ """
71
+
72
+ if self.stream.open_stream is None:
73
+ return False
74
+ raw = str(os.environ.get("CIEL_RUNTIME_COLLECT_STREAM") or "").strip().lower()
75
+ return raw not in {"0", "off", "false", "no", "disable", "disabled"}
76
+
77
+ def opened_stream_collector(
78
+ self, parse: Callable[..., Any], operation: str
79
+ ) -> Callable[..., dict[str, Any]] | None:
80
+ """A drop-in replacement for the collection path's blocking POST."""
81
+
82
+ if not self.streaming_collection_enabled():
83
+ return None
84
+
85
+ def collect(
86
+ url: str,
87
+ req_body: dict[str, Any],
88
+ headers: dict[str, str],
89
+ timeout: float,
90
+ provider: str,
91
+ pcfg: dict[str, Any],
92
+ model: str,
93
+ *,
94
+ retry_rate_limits: bool = True,
95
+ ) -> dict[str, Any]:
96
+ resp = self.stream.open_stream(
97
+ url, req_body, headers, timeout, provider, pcfg, model, None,
98
+ retry_rate_limits=retry_rate_limits,
99
+ )
100
+ try:
101
+ collection = parse(resp, self.stream.policy())
102
+ finally:
103
+ try:
104
+ resp.close()
105
+ except Exception:
106
+ pass
107
+ return self.report_collected(collection, operation, provider, model)
108
+
109
+ return collect
110
+
111
+ def report_collected(
112
+ self, collection: Any, operation: str, provider: str, model: str
113
+ ) -> dict[str, Any]:
114
+ if collection.verdict is not None:
115
+ self.stream.log(
116
+ "WARN",
117
+ f"{operation}_collect_runaway_repetition provider={provider} "
118
+ f"model={model} chunks={collection.chunks} "
119
+ f"{collection.verdict.log_fields()}",
120
+ )
121
+ return collection.response
122
+
123
+ def anthropic_stream_collector(self) -> Callable[..., dict[str, Any]] | None:
124
+ """The Anthropic collector opens its own request, so it hands one back."""
125
+
126
+ if not self.streaming_collection_enabled():
127
+ return None
128
+
129
+ def collect(resp: Any, provider: str, model: str) -> dict[str, Any]:
130
+ return self.report_collected(
131
+ collect_anthropic_message_stream(resp, self.stream.policy()),
132
+ "anthropic",
133
+ provider,
134
+ model,
135
+ )
136
+
137
+ return collect
138
+
139
+ @staticmethod
140
+ def identity_upstream_model(
141
+ provider: str,
142
+ pcfg: dict[str, Any],
143
+ model: str,
144
+ ) -> str:
145
+ del provider, pcfg
146
+ return model
147
+
148
+ def build_ollama_request(
149
+ self,
150
+ provider: str,
151
+ model: str,
152
+ body: dict[str, Any],
153
+ pcfg: dict[str, Any],
154
+ *,
155
+ stream: bool,
156
+ ) -> dict[str, Any]:
157
+ return self.strategies.ollama_request(
158
+ model,
159
+ body,
160
+ pcfg,
161
+ stream=stream,
162
+ provider=provider,
163
+ )
164
+
165
+ def collect_ollama(
166
+ self,
167
+ handler: BaseHTTPRequestHandler,
168
+ provider: str,
169
+ pcfg: dict[str, Any],
170
+ body: dict[str, Any],
171
+ ) -> dict[str, Any]:
172
+ strategy = ChatCollectionStrategy(
173
+ operation="ollama_chat",
174
+ build_request=self.build_ollama_request,
175
+ decode_response=self.strategies.ollama_decode,
176
+ request_timeout_seconds=self.strategies.ollama_timeout,
177
+ normalize_upstream_model=self.identity_upstream_model,
178
+ skip_rate_limit_during_compatibility_test=True,
179
+ stream_collect=self.opened_stream_collector(
180
+ collect_ollama_chat_stream, "ollama"
181
+ ),
182
+ )
183
+ return collect_chat_message_for_responses(
184
+ handler,
185
+ provider,
186
+ pcfg,
187
+ body,
188
+ strategy=strategy,
189
+ services=self.shared,
190
+ )
191
+
192
+ def collect_openai_chat(
193
+ self,
194
+ handler: BaseHTTPRequestHandler,
195
+ provider: str,
196
+ pcfg: dict[str, Any],
197
+ body: dict[str, Any],
198
+ ) -> dict[str, Any]:
199
+ strategy = ChatCollectionStrategy(
200
+ operation="openai_chat",
201
+ build_request=self.strategies.openai_request,
202
+ decode_response=self.strategies.openai_decode,
203
+ request_timeout_seconds=self.strategies.openai_timeout,
204
+ normalize_upstream_model=self.strategies.upstream_model,
205
+ stream_collect=self.opened_stream_collector(
206
+ collect_openai_chat_stream, "openai_chat"
207
+ ),
208
+ )
209
+ return collect_chat_message_for_responses(
210
+ handler,
211
+ provider,
212
+ pcfg,
213
+ body,
214
+ strategy=strategy,
215
+ services=self.shared,
216
+ )
217
+
218
+ def collect_anthropic(
219
+ self,
220
+ handler: BaseHTTPRequestHandler,
221
+ provider: str,
222
+ pcfg: dict[str, Any],
223
+ body: dict[str, Any],
224
+ ) -> dict[str, Any]:
225
+ return collect_anthropic_message_for_responses(
226
+ handler,
227
+ provider,
228
+ pcfg,
229
+ body,
230
+ services=self.anthropic,
231
+ stream_collect=self.anthropic_stream_collector(),
232
+ )
233
+
234
+ def collect(
235
+ self,
236
+ handler: BaseHTTPRequestHandler,
237
+ provider: str,
238
+ pcfg: dict[str, Any],
239
+ body: dict[str, Any],
240
+ ) -> dict[str, Any]:
241
+ upstream_model = self.routing.resolve_model(
242
+ provider, pcfg, body.get("model")
243
+ )
244
+ protocol = self.routing.select_protocol(
245
+ provider,
246
+ pcfg,
247
+ "openai_responses",
248
+ upstream_model,
249
+ )
250
+ collectors = {
251
+ "ollama_chat": self.collect_ollama,
252
+ "openai_chat": self.collect_openai_chat,
253
+ "anthropic_messages": self.collect_anthropic,
254
+ }
255
+ collector = collectors.get(protocol)
256
+ if collector is None:
257
+ provider_label = self.routing.provider_labels.get(provider, provider)
258
+ endpoint_family = protocol.replace("_", "-")
259
+ raise RuntimeError(
260
+ f"{provider_label} model {upstream_model!r} uses the "
261
+ f"{endpoint_family} endpoint family. ciel-runtime currently routes "
262
+ f"{provider_label} /v1/messages and /v1/chat/completions models."
263
+ )
264
+ return self.collect_without_runaway(
265
+ collector, handler, provider, pcfg, body, upstream_model
266
+ )
267
+
268
+ def collect_without_runaway(
269
+ self,
270
+ collector: Callable[..., dict[str, Any]],
271
+ handler: BaseHTTPRequestHandler,
272
+ provider: str,
273
+ pcfg: dict[str, Any],
274
+ body: dict[str, Any],
275
+ upstream_model: str,
276
+ ) -> dict[str, Any]:
277
+ """Re-issue a looped turn instead of handing the loop to the client.
278
+
279
+ Nothing has been written to the client at this point and no tool call
280
+ has been executed, so a discarded attempt has no side effects -- unlike
281
+ the streaming paths, this one can simply ask again. Sampling is
282
+ stochastic (DeepSeek ships ``do_sample: true, temperature: 1.0``), so a
283
+ plain retry is a genuinely different draw; the ladder then lowers
284
+ reasoning effort, which is DeepSeek's own advice for this failure.
285
+ """
286
+
287
+ policy = policy_from_env(os.environ.get)
288
+ ladder = self.effort_ladder(policy)
289
+ attempt_body = body
290
+ message: dict[str, Any] = {}
291
+ trimmed: Any = None
292
+ for attempt, _effort in enumerate(ladder):
293
+ message = collector(handler, provider, pcfg, attempt_body)
294
+ trimmed, verdict = trim_runaway_message_content(
295
+ message.get("content") if isinstance(message, dict) else None, policy
296
+ )
297
+ if verdict is None:
298
+ return message
299
+ last_attempt = attempt + 1 >= len(ladder)
300
+ self.stream.log(
301
+ "WARN",
302
+ f"collect_runaway_repetition provider={provider} model={upstream_model} "
303
+ f"attempt={attempt + 1}/{len(ladder)} retry={not last_attempt} "
304
+ f"{verdict.log_fields()}",
305
+ )
306
+ if last_attempt:
307
+ break
308
+ attempt_body = self.with_reasoning_effort(body, ladder[attempt + 1])
309
+ return {**message, "content": trimmed, "stop_reason": "max_tokens"}
310
+
311
+ @staticmethod
312
+ def effort_ladder(policy: RunawayOutputPolicy) -> list[str | None]:
313
+ """Attempts to make, and the reasoning effort each one asks for."""
314
+
315
+ raw = os.environ.get("CIEL_RUNTIME_RUNAWAY_RETRIES")
316
+ try:
317
+ retries = int(str(raw).strip())
318
+ except (TypeError, ValueError):
319
+ retries = 2
320
+ if not policy.enabled or not policy.recover:
321
+ retries = 0
322
+ retries = max(0, min(4, retries))
323
+ # The loop happens inside reasoning, so lowering effort is the lever
324
+ # that changes the odds; "low" turns thinking off for DeepSeek entirely.
325
+ return ([None] + ["high", "low", "low", "low"])[: retries + 1]
326
+
327
+ @staticmethod
328
+ def with_reasoning_effort(body: dict[str, Any], effort: str | None) -> dict[str, Any]:
329
+ """Ask the next attempt for less reasoning via the internal effort hint.
330
+
331
+ Both ``OllamaThinkingPolicy`` and the DeepSeek adapter already read this
332
+ metadata key, so one hint covers every collected protocol.
333
+ """
334
+
335
+ if not effort or not isinstance(body, dict):
336
+ return body
337
+ metadata = body.get("metadata")
338
+ projected = dict(metadata) if isinstance(metadata, dict) else {}
339
+ projected[INTERNAL_REASONING_EFFORT_KEY] = effort
340
+ return {**body, "metadata": projected}
341
+
342
+ @staticmethod
343
+ def guard_runaway(message: dict[str, Any]) -> dict[str, Any]:
344
+ """Trim a repetition loop out of one collected message.
345
+
346
+ Kept as the single-shot form used by callers that cannot retry.
347
+ """
348
+
349
+ if not isinstance(message, dict):
350
+ return message
351
+ content, verdict = trim_runaway_message_content(
352
+ message.get("content"), policy_from_env(os.environ.get)
353
+ )
354
+ if verdict is None:
355
+ return message
356
+ return {**message, "content": content, "stop_reason": "max_tokens"}
357
+
358
+
359
+ @dataclass(frozen=True, slots=True)
360
+ class ResponseCollectionCompatibilityApi:
361
+ context: Callable[[], ResponseCollectionContext]
362
+
363
+ def services(self) -> ResponseCollectionServices:
364
+ return self.context().shared
365
+
366
+ def identity_upstream_model(self, *args: Any, **kwargs: Any) -> str:
367
+ return self.context().identity_upstream_model(*args, **kwargs)
368
+
369
+ def build_ollama_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
370
+ return self.context().build_ollama_request(*args, **kwargs)
371
+
372
+ def collect_ollama(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
373
+ return self.context().collect_ollama(*args, **kwargs)
374
+
375
+ def collect_openai_chat(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
376
+ return self.context().collect_openai_chat(*args, **kwargs)
377
+
378
+ def collect_anthropic(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
379
+ return self.context().collect_anthropic(*args, **kwargs)
380
+
381
+ def collect(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
382
+ return self.context().collect(*args, **kwargs)
383
+
384
+
385
+ __all__ = [
386
+ "ResponseCollectionCompatibilityApi",
387
+ "ResponseCollectionContext",
388
+ "ResponseCollectionRoutingPorts",
389
+ "ResponseCollectionStreamPorts",
390
+ "ResponseCollectionStrategyPorts",
391
+ ]