dinkster-api 0.0.2__py3-none-any.whl

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 (238) hide show
  1. dinkster_api/__init__.py +9 -0
  2. dinkster_api/py.typed +0 -0
  3. dinkster_api/v1.py +935 -0
  4. dinkster_api-0.0.2.dist-info/METADATA +16 -0
  5. dinkster_api-0.0.2.dist-info/RECORD +238 -0
  6. dinkster_api-0.0.2.dist-info/WHEEL +4 -0
  7. dinkster_api-0.0.2.dist-info/entry_points.txt +2 -0
  8. dinkster_assets/__init__.py +337 -0
  9. dinkster_assets/acquire.py +226 -0
  10. dinkster_assets/acquisition_plan.py +145 -0
  11. dinkster_assets/audio.py +119 -0
  12. dinkster_assets/catalog.py +156 -0
  13. dinkster_assets/component_manifest.py +110 -0
  14. dinkster_assets/declared.py +218 -0
  15. dinkster_assets/fetch.py +495 -0
  16. dinkster_assets/guess.py +136 -0
  17. dinkster_assets/identity.py +72 -0
  18. dinkster_assets/integrity.py +310 -0
  19. dinkster_assets/json_metadata.py +41 -0
  20. dinkster_assets/kind.py +52 -0
  21. dinkster_assets/latent_format.py +346 -0
  22. dinkster_assets/library.py +816 -0
  23. dinkster_assets/media.py +1385 -0
  24. dinkster_assets/migrations/env.py +38 -0
  25. dinkster_assets/migrations/script.py.mako +28 -0
  26. dinkster_assets/migrations/versions/0001_asset_records_create_asset_record_catalog.py +109 -0
  27. dinkster_assets/migrations/versions/0002_asset_observations.py +36 -0
  28. dinkster_assets/model.py +242 -0
  29. dinkster_assets/mounts.py +1278 -0
  30. dinkster_assets/need.py +168 -0
  31. dinkster_assets/pack_assets.py +217 -0
  32. dinkster_assets/probe.py +118 -0
  33. dinkster_assets/provenance.py +254 -0
  34. dinkster_assets/public_acquisition.py +424 -0
  35. dinkster_assets/py.typed +0 -0
  36. dinkster_assets/records.py +899 -0
  37. dinkster_assets/resolution.py +778 -0
  38. dinkster_assets/resolver_index.py +364 -0
  39. dinkster_assets/resolver_subscription.py +1149 -0
  40. dinkster_assets/save_target.py +139 -0
  41. dinkster_assets/schema.py +73 -0
  42. dinkster_assets/selections.py +256 -0
  43. dinkster_assets/transport.py +243 -0
  44. dinkster_assets/value.py +258 -0
  45. dinkster_assets/vault.py +276 -0
  46. dinkster_assets/writer.py +765 -0
  47. dinkster_caches/__init__.py +19 -0
  48. dinkster_caches/cas.py +310 -0
  49. dinkster_caches/disk.py +330 -0
  50. dinkster_caches/layered.py +150 -0
  51. dinkster_caches/memory.py +208 -0
  52. dinkster_caches/py.typed +0 -0
  53. dinkster_caches/wire.py +346 -0
  54. dinkster_image_document/__init__.py +39 -0
  55. dinkster_image_document/blend.py +264 -0
  56. dinkster_image_document/compat.py +211 -0
  57. dinkster_image_document/document.py +374 -0
  58. dinkster_image_document/format.py +569 -0
  59. dinkster_image_document/interchange.py +572 -0
  60. dinkster_image_document/py.typed +1 -0
  61. dinkster_image_document/render.py +455 -0
  62. dinkster_inference_wire/__init__.py +141 -0
  63. dinkster_inference_wire/camera_wire.py +127 -0
  64. dinkster_inference_wire/conditioning.py +752 -0
  65. dinkster_inference_wire/conditioning_wire.py +1143 -0
  66. dinkster_inference_wire/devices.py +285 -0
  67. dinkster_inference_wire/generation.py +446 -0
  68. dinkster_inference_wire/hook_values.py +168 -0
  69. dinkster_inference_wire/ic_lora_parameters.py +69 -0
  70. dinkster_inference_wire/latents.py +244 -0
  71. dinkster_inference_wire/media_wire.py +169 -0
  72. dinkster_inference_wire/openai_generation.py +1035 -0
  73. dinkster_inference_wire/pose_keypoint.py +83 -0
  74. dinkster_inference_wire/process.py +33 -0
  75. dinkster_inference_wire/py.typed +0 -0
  76. dinkster_inference_wire/registry.py +116 -0
  77. dinkster_inference_wire/sam3_wire.py +68 -0
  78. dinkster_inference_wire/sampling_wire.py +279 -0
  79. dinkster_inference_wire/sources.py +399 -0
  80. dinkster_inference_wire/spaces.py +344 -0
  81. dinkster_inference_wire/sparse.py +372 -0
  82. dinkster_inference_wire/splat_wire.py +23 -0
  83. dinkster_inference_wire/types.py +256 -0
  84. dinkster_inference_wire/vision_wire.py +53 -0
  85. dinkster_inference_wire/voxel_wire.py +72 -0
  86. dinkster_inference_wire/wan_wire.py +93 -0
  87. dinkster_inference_wire/weights.py +219 -0
  88. dinkster_memory/__init__.py +92 -0
  89. dinkster_memory/accelerator.py +96 -0
  90. dinkster_memory/budgets.py +96 -0
  91. dinkster_memory/details.py +91 -0
  92. dinkster_memory/governor.py +554 -0
  93. dinkster_memory/leases.py +176 -0
  94. dinkster_memory/py.typed +0 -0
  95. dinkster_memory/release.py +131 -0
  96. dinkster_memory/reported.py +101 -0
  97. dinkster_memory/reservations.py +101 -0
  98. dinkster_memory/resident.py +600 -0
  99. dinkster_memory/system.py +614 -0
  100. dinkster_memory/tenants.py +67 -0
  101. dinkster_protocol/__init__.py +1008 -0
  102. dinkster_protocol/attention.py +652 -0
  103. dinkster_protocol/extension_contribution_kinds.json +21 -0
  104. dinkster_protocol/extension_contribution_kinds.py +43 -0
  105. dinkster_protocol/extensions.py +742 -0
  106. dinkster_protocol/frontend_modules.py +145 -0
  107. dinkster_protocol/multigpu.py +107 -0
  108. dinkster_protocol/pack_surfaces.py +489 -0
  109. dinkster_protocol/payload.py +63 -0
  110. dinkster_protocol/preview.py +86 -0
  111. dinkster_protocol/py.typed +0 -0
  112. dinkster_protocol/result_algebra.py +380 -0
  113. dinkster_protocol/result_document.py +244 -0
  114. dinkster_protocol/run_control.py +96 -0
  115. dinkster_protocol/training.py +399 -0
  116. dinkster_protocol/workgroup.py +1014 -0
  117. dinkster_registry/__init__.py +135 -0
  118. dinkster_registry/admission.py +372 -0
  119. dinkster_registry/artifact.py +128 -0
  120. dinkster_registry/composition.py +152 -0
  121. dinkster_registry/grants.py +206 -0
  122. dinkster_registry/install.py +1017 -0
  123. dinkster_registry/model.py +414 -0
  124. dinkster_registry/principals.py +494 -0
  125. dinkster_registry/py.typed +0 -0
  126. dinkster_schema/__init__.py +347 -0
  127. dinkster_schema/capture.py +240 -0
  128. dinkster_schema/coerce.py +186 -0
  129. dinkster_schema/comfy_alias.py +496 -0
  130. dinkster_schema/comfy_group.py +768 -0
  131. dinkster_schema/elaborate.py +676 -0
  132. dinkster_schema/execution.py +24 -0
  133. dinkster_schema/expansion.py +123 -0
  134. dinkster_schema/log.py +206 -0
  135. dinkster_schema/media.py +77 -0
  136. dinkster_schema/model.py +2920 -0
  137. dinkster_schema/names.py +25 -0
  138. dinkster_schema/node.py +137 -0
  139. dinkster_schema/output_descriptors.py +63 -0
  140. dinkster_schema/py.typed +0 -0
  141. dinkster_schema/replace.py +1853 -0
  142. dinkster_schema/reporting.py +441 -0
  143. dinkster_schema/solve.py +178 -0
  144. dinkster_schema/wire.py +1876 -0
  145. dinkster_values/__init__.py +625 -0
  146. dinkster_values/_media_array.py +7 -0
  147. dinkster_values/absent.py +77 -0
  148. dinkster_values/asset_folders.py +64 -0
  149. dinkster_values/assets.py +106 -0
  150. dinkster_values/audio_codec.py +594 -0
  151. dinkster_values/audio_lazy.py +871 -0
  152. dinkster_values/builtin_types.py +181 -0
  153. dinkster_values/curve.py +260 -0
  154. dinkster_values/detection_codec.py +286 -0
  155. dinkster_values/image_codec.py +691 -0
  156. dinkster_values/image_components.py +219 -0
  157. dinkster_values/latent_codec.py +514 -0
  158. dinkster_values/limits.py +52 -0
  159. dinkster_values/lists.py +216 -0
  160. dinkster_values/media_containers.py +70 -0
  161. dinkster_values/model.py +417 -0
  162. dinkster_values/model3d_codec.py +167 -0
  163. dinkster_values/names.py +168 -0
  164. dinkster_values/pins.py +77 -0
  165. dinkster_values/py.typed +0 -0
  166. dinkster_values/registry.py +1105 -0
  167. dinkster_values/resident.py +210 -0
  168. dinkster_values/resources.py +210 -0
  169. dinkster_values/splat_codec.py +477 -0
  170. dinkster_values/storage.py +133 -0
  171. dinkster_values/streams.py +241 -0
  172. dinkster_values/svg_codec.py +120 -0
  173. dinkster_values/timeline_video.py +68 -0
  174. dinkster_values/tuples.py +61 -0
  175. dinkster_values/video_codec.py +897 -0
  176. dinkster_values/video_document.py +315 -0
  177. dinkster_values/video_edits.py +253 -0
  178. dinkster_values/video_probe.py +221 -0
  179. dinkster_video/__init__.py +58 -0
  180. dinkster_video/document.py +352 -0
  181. dinkster_video/formats.py +722 -0
  182. dinkster_video/image_math.py +175 -0
  183. dinkster_video/preview/dinkster-pack.toml +36 -0
  184. dinkster_video/preview/src/dinkster_video_preview/__init__.py +84 -0
  185. dinkster_video/preview/src/dinkster_video_preview/preview.js +45 -0
  186. dinkster_video/py.typed +1 -0
  187. dinkster_video/runtime.py +1561 -0
  188. dinkster_video/timeline.py +576 -0
  189. dinkster_video/timeline_runtime.py +305 -0
  190. dinkster_video_preview/__init__.py +84 -0
  191. dinkster_video_preview/preview.js +45 -0
  192. dinkster_workers/__init__.py +207 -0
  193. dinkster_workers/_doctor_probe.py +359 -0
  194. dinkster_workers/accelerator.py +156 -0
  195. dinkster_workers/aimdo_bootstrap.py +81 -0
  196. dinkster_workers/backend_env.py +1485 -0
  197. dinkster_workers/blobs.py +402 -0
  198. dinkster_workers/boundary.py +1918 -0
  199. dinkster_workers/catalog.py +182 -0
  200. dinkster_workers/devices.py +164 -0
  201. dinkster_workers/diagnostics.py +64 -0
  202. dinkster_workers/dispatch.py +298 -0
  203. dinkster_workers/doctor.py +1382 -0
  204. dinkster_workers/egress.py +344 -0
  205. dinkster_workers/error_hints.py +175 -0
  206. dinkster_workers/execution.py +160 -0
  207. dinkster_workers/governed.py +123 -0
  208. dinkster_workers/headroom.py +156 -0
  209. dinkster_workers/host.py +3116 -0
  210. dinkster_workers/in_process.py +1325 -0
  211. dinkster_workers/interpreter.py +84 -0
  212. dinkster_workers/isolated.py +862 -0
  213. dinkster_workers/launch.py +72 -0
  214. dinkster_workers/manifest.py +3332 -0
  215. dinkster_workers/media.py +196 -0
  216. dinkster_workers/pack_environment.py +473 -0
  217. dinkster_workers/pack_lock.py +359 -0
  218. dinkster_workers/placement.py +215 -0
  219. dinkster_workers/preview_artifacts.py +233 -0
  220. dinkster_workers/produced_assets.py +218 -0
  221. dinkster_workers/provision.py +597 -0
  222. dinkster_workers/py.typed +0 -0
  223. dinkster_workers/relay.py +762 -0
  224. dinkster_workers/remote.py +921 -0
  225. dinkster_workers/resume.py +490 -0
  226. dinkster_workers/routing.py +134 -0
  227. dinkster_workers/run_control.py +275 -0
  228. dinkster_workers/sandbox.py +798 -0
  229. dinkster_workers/saved_artifacts.py +275 -0
  230. dinkster_workers/service.py +593 -0
  231. dinkster_workers/session.py +2605 -0
  232. dinkster_workers/single_job_multi_gpu.py +7 -0
  233. dinkster_workers/staging.py +230 -0
  234. dinkster_workers/transport.py +161 -0
  235. dinkster_workers/workgroup.py +729 -0
  236. dinkster_workers/workgroup_compose.py +135 -0
  237. dinkster_workers/workgroup_config.py +196 -0
  238. dinkster_workers/workgroup_session.py +340 -0
@@ -0,0 +1,275 @@
1
+ """Run-scoped control relay for boundary workers.
2
+
3
+ The relay carries requests, not callbacks. A request is always tied to the
4
+ boundary invocation that owns it, so a completed or cancelled invocation
5
+ cannot address a later run which happens to reuse a node id.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
13
+ from multiprocessing.shared_memory import SharedMemory
14
+ from types import MappingProxyType
15
+ from typing import Any, cast
16
+
17
+ from dinkster_protocol import InvocationResult
18
+ from dinkster_schema import RunExecutionContext
19
+ from dinkster_values import Value
20
+
21
+ from .boundary import BoundaryError, ValueCodec, encode_result, release_segment
22
+
23
+ Send = Callable[[dict[str, object], Sequence[bytes], Sequence[SharedMemory]], Awaitable[None]]
24
+
25
+
26
+ def _document(value: Mapping[str, object]) -> bytes:
27
+ return json.dumps(value, default=dict, separators=(",", ":")).encode("utf-8")
28
+
29
+
30
+ def _freeze(value: object) -> object:
31
+ if isinstance(value, dict):
32
+ return MappingProxyType(
33
+ {key: _freeze(item) for key, item in cast("dict[str, object]", value).items()}
34
+ )
35
+ if isinstance(value, list):
36
+ return tuple(_freeze(item) for item in cast("list[object]", value))
37
+ return value
38
+
39
+
40
+ class BoundaryRunContext:
41
+ """Worker-local proxy for the parent invocation's live run context."""
42
+
43
+ def __init__(
44
+ self,
45
+ invocation_id: str,
46
+ node_id: str,
47
+ inputs: Mapping[str, Value],
48
+ codec: ValueCodec,
49
+ send: Send,
50
+ suspend: Callable[[], Awaitable[None]],
51
+ resume: Callable[[], Awaitable[None]],
52
+ prepare_values: Callable[[Mapping[str, Value]], Awaitable[Mapping[str, Value]]],
53
+ ) -> None:
54
+ self._invocation_id = invocation_id
55
+ self._node_id = node_id
56
+ self._inputs = inputs
57
+ self._codec = codec
58
+ self._send = send
59
+ self._suspend = suspend
60
+ self._resume = resume
61
+ self._prepare_values = prepare_values
62
+ self._wait_lock = asyncio.Lock()
63
+ self._pending: dict[str, asyncio.Future[tuple[dict[str, Any], list[bytes]]]] = {}
64
+ self._completed: set[str] = set()
65
+ self._index = 0
66
+ self._closed = False
67
+
68
+ @property
69
+ def node_id(self) -> str:
70
+ return self._node_id
71
+
72
+ @property
73
+ def inputs(self) -> Mapping[str, Value]:
74
+ return self._inputs
75
+
76
+ def close(self) -> None:
77
+ self._closed = True
78
+ for future in self._pending.values():
79
+ if not future.done():
80
+ future.cancel()
81
+ self._pending.clear()
82
+ self._completed.clear()
83
+
84
+ def accept(self, header: dict[str, Any], blobs: list[bytes]) -> bool:
85
+ request_id = header.get("requestId")
86
+ if type(request_id) is not str or header.get("invocationId") != self._invocation_id:
87
+ return False
88
+ future = self._pending.pop(request_id, None)
89
+ if future is None:
90
+ return request_id in self._completed
91
+ if future.done():
92
+ return False
93
+ future.set_result((header, blobs))
94
+ self._completed.add(request_id)
95
+ return True
96
+
97
+ async def _request(
98
+ self,
99
+ operation: str,
100
+ fields: Mapping[str, object] = {},
101
+ blobs: Sequence[bytes] = (),
102
+ segments: Sequence[SharedMemory] = (),
103
+ ) -> tuple[dict[str, Any], list[bytes]]:
104
+ if self._closed:
105
+ for segment in segments:
106
+ release_segment(segment)
107
+ raise RuntimeError("execution context is no longer active")
108
+ request_id = f"{self._invocation_id}:control:{self._index}"
109
+ self._index += 1
110
+ future: asyncio.Future[tuple[dict[str, Any], list[bytes]]] = (
111
+ asyncio.get_running_loop().create_future()
112
+ )
113
+ self._pending[request_id] = future
114
+ try:
115
+ await self._send(
116
+ {
117
+ "type": "runControl",
118
+ "invocationId": self._invocation_id,
119
+ "requestId": request_id,
120
+ "operation": operation,
121
+ **fields,
122
+ },
123
+ blobs,
124
+ segments,
125
+ )
126
+ header, reply_blobs = await future
127
+ finally:
128
+ self._pending.pop(request_id, None)
129
+ error = header.get("error")
130
+ if type(error) is str:
131
+ if header.get("errorType") == "ValueError":
132
+ raise ValueError(error)
133
+ raise RuntimeError(error)
134
+ return header, reply_blobs
135
+
136
+ async def get_graph(self) -> Mapping[str, object]:
137
+ _, blobs = await self._request("getGraph")
138
+ if len(blobs) != 1:
139
+ raise BoundaryError("run control graph response is malformed")
140
+ graph = json.loads(blobs[0])
141
+ if not isinstance(graph, Mapping):
142
+ raise BoundaryError("run control graph response is not a mapping")
143
+ return cast("Mapping[str, object]", _freeze(cast("Mapping[str, object]", graph)))
144
+
145
+ async def await_run_value(self, node_id: str) -> Mapping[str, Value]:
146
+ async with self._wait_lock:
147
+ await self._suspend()
148
+ try:
149
+ header, blobs = await self._request("awaitRunValue", {"nodeId": node_id})
150
+ except asyncio.CancelledError:
151
+ raise
152
+ except BaseException:
153
+ if not self._closed:
154
+ await self._resume()
155
+ raise
156
+ if self._closed:
157
+ raise RuntimeError("execution context is no longer active")
158
+ await self._resume()
159
+ values = header.get("values")
160
+ if not isinstance(values, Mapping):
161
+ raise BoundaryError("run control value response is malformed")
162
+ typed_values = cast("Mapping[object, object]", values)
163
+ consumed: list[str] = []
164
+ result: dict[str, Value] = {
165
+ name: self._codec.decode(cast("Mapping[str, Any]", wire), blobs, consumed)[0]
166
+ for name, wire in typed_values.items()
167
+ if type(name) is str and isinstance(wire, Mapping)
168
+ }
169
+ if len(result) != len(typed_values):
170
+ raise BoundaryError("run control value response is malformed")
171
+ if consumed:
172
+ await self._send({"type": "shmAck", "segments": consumed}, [], [])
173
+ return result
174
+
175
+ async def publish_run_value(self, node_id: str, value: Mapping[str, Value]) -> None:
176
+ if self._closed:
177
+ raise RuntimeError("execution context is no longer active")
178
+ value = await self._prepare_values(value)
179
+ with self._codec.suspend_persistent_cas():
180
+ encoded, blobs, segments = encode_result(
181
+ self._codec,
182
+ InvocationResult(outputs=value),
183
+ self._invocation_id,
184
+ 0,
185
+ )
186
+ await self._request(
187
+ "publishRunValue", {"nodeId": node_id, "values": encoded["outputs"]}, blobs, segments
188
+ )
189
+
190
+ async def inhibit(self, node_ids: Sequence[str]) -> None:
191
+ await self._request("inhibit", {"nodeIds": list(node_ids)})
192
+
193
+ async def override_node(self, node_id: str, inputs: Mapping[str, object]) -> None:
194
+ await self._request("overrideNode", {"nodeId": node_id}, [_document(inputs)])
195
+
196
+
197
+ async def dispatch_run_control(
198
+ context: RunExecutionContext,
199
+ codec: ValueCodec,
200
+ header: Mapping[str, Any],
201
+ blobs: list[bytes],
202
+ send: Send,
203
+ publish: Callable[[str, Mapping[str, Value]], Awaitable[None]],
204
+ prepare_values: Callable[[Mapping[str, Value]], Awaitable[None]],
205
+ ) -> None:
206
+ """Run one parent-side request and send a single reply."""
207
+ request_id = header.get("requestId")
208
+ invocation_id = header.get("invocationId")
209
+ if type(request_id) is not str or type(invocation_id) is not str:
210
+ return
211
+ reply: dict[str, object] = {
212
+ "type": "runControlResult",
213
+ "invocationId": invocation_id,
214
+ "requestId": request_id,
215
+ }
216
+ reply_blobs: list[bytes] = []
217
+ segments: list[SharedMemory] = []
218
+ consumed: list[str] = []
219
+ try:
220
+ operation = header.get("operation")
221
+ if operation == "getGraph":
222
+ reply_blobs.append(_document(await context.get_graph()))
223
+ elif operation == "awaitRunValue":
224
+ node_id = header.get("nodeId")
225
+ if type(node_id) is not str:
226
+ raise BoundaryError("run control node id is malformed")
227
+ published = await context.await_run_value(node_id)
228
+ await prepare_values(published)
229
+ with codec.suspend_persistent_cas():
230
+ encoded, reply_blobs, segments = encode_result(
231
+ codec,
232
+ InvocationResult(outputs=published),
233
+ invocation_id,
234
+ 0,
235
+ )
236
+ reply["values"] = encoded["outputs"]
237
+ elif operation == "publishRunValue":
238
+ raw_values = header.get("values")
239
+ node_id = header.get("nodeId")
240
+ if type(node_id) is not str or not isinstance(raw_values, Mapping):
241
+ raise BoundaryError("run control publish request is malformed")
242
+ typed_values = cast("Mapping[object, object]", raw_values)
243
+ values: dict[str, Value] = {
244
+ name: codec.decode(cast("Mapping[str, Any]", wire), blobs, consumed)[0]
245
+ for name, wire in typed_values.items()
246
+ if type(name) is str and isinstance(wire, Mapping)
247
+ }
248
+ if len(values) != len(typed_values):
249
+ raise BoundaryError("run control publish request is malformed")
250
+ await publish(node_id, values)
251
+ elif operation == "inhibit":
252
+ node_ids_raw = header.get("nodeIds")
253
+ if not isinstance(node_ids_raw, list):
254
+ raise BoundaryError("run control inhibit request is malformed")
255
+ node_id_items = cast("list[object]", node_ids_raw)
256
+ if not all(type(item) is str for item in node_id_items):
257
+ raise BoundaryError("run control inhibit request is malformed")
258
+ node_ids = cast("list[str]", cast("object", node_id_items))
259
+ await context.inhibit(node_ids)
260
+ elif operation == "overrideNode":
261
+ node_id = header.get("nodeId")
262
+ if type(node_id) is not str or len(blobs) != 1:
263
+ raise BoundaryError("run control override request is malformed")
264
+ inputs = json.loads(blobs[0])
265
+ if not isinstance(inputs, Mapping):
266
+ raise BoundaryError("run control override inputs are malformed")
267
+ await context.override_node(node_id, cast("Mapping[str, object]", inputs))
268
+ else:
269
+ raise BoundaryError("unknown run control operation")
270
+ except Exception as exc: # node-facing failures retain their normal messages
271
+ reply["error"] = str(exc)
272
+ reply["errorType"] = "ValueError" if isinstance(exc, ValueError) else "RuntimeError"
273
+ if consumed:
274
+ await send({"type": "shmAck", "segments": consumed}, [], [])
275
+ await send(reply, reply_blobs, segments)