induscode 0.1.0__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 (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,397 @@
1
+ """RuntimeBroker — the single decision point that routes every turn to
2
+ either an external runtime (a spawned child coding-agent driven by a
3
+ :class:`~induscode.runtime_bridge.contract.RuntimeBridge`) or the framework
4
+ network stream (:func:`indusagi.ai.stream_simple` over an HTTP provider)
5
+ (port of TS ``src/runtime-bridge/broker.ts``).
6
+
7
+ The broker is the only component the product asks "how is this turn
8
+ produced?". It owns three concerns:
9
+
10
+ 1. **Routing.** :meth:`_Broker.route` resolves the model's optional
11
+ :class:`~induscode.runtime_bridge.contract.ExternalRuntimeSpec` (a
12
+ ``bridge:<adapter>`` baseUrl decode) and a matching registered bridge. A
13
+ model with a spec whose adapter is registered routes ``"external"``;
14
+ everything else routes ``"framework"``.
15
+ 2. **Dispatch.** :meth:`RuntimeBrokerRuntime.exchange` acts on that
16
+ decision: for an external route it builds (or is injected) a
17
+ :class:`~induscode.runtime_bridge.contract.ChildTransport` and calls
18
+ :meth:`RuntimeBridge.run_exchange` over it; for a framework route it
19
+ calls the framework ``stream_simple``. Either way it returns the
20
+ framework ``AssistantMessageEventStream`` the turn streams into — the two
21
+ paths are indistinguishable to the caller.
22
+ 3. **Resume persistence.** When a bridge surfaces a ``resume`` token (a CLI
23
+ session id / thread id), the broker persists it through an injected
24
+ :class:`~induscode.runtime_bridge.contract.RuntimeLinkStore` as a
25
+ *renamed* custom transcript entry —
26
+ :data:`~induscode.runtime_bridge.contract.RUNTIME_LINK_ENTRY` =
27
+ ``"external-runtime-link"``, shape ``{source, bridge, resumeToken, at}``
28
+ — so a later exchange can reattach the same underlying session. Handle
29
+ reuse keys on the composite :func:`runtime_source_key`
30
+ (``source|model|bridge``), not three separate field comparisons.
31
+
32
+ The ``ChildTransport`` is injectable end to end: a production factory wraps
33
+ a spawned process, a test factory returns a hand-written fake. The broker
34
+ never imports a subprocess module; nothing here spawns a real
35
+ ``claude``/``codex`` binary.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import asyncio
41
+ import inspect
42
+ from collections.abc import Callable, Sequence
43
+ from dataclasses import dataclass, replace
44
+ from datetime import datetime, timezone
45
+ from types import SimpleNamespace
46
+ from typing import Any, Protocol
47
+
48
+ from indusagi.ai import stream_simple
49
+
50
+ from induscode.runtime_bridge.bridges import spec_from_model
51
+ from induscode.runtime_bridge.contract import (
52
+ AssistantMessageEventStream,
53
+ ChildTransportFactory,
54
+ Context,
55
+ ExchangeOptions,
56
+ ExternalRoute,
57
+ ExternalRuntimeSpec,
58
+ FrameworkRoute,
59
+ FrameworkStream,
60
+ Model,
61
+ RuntimeBridge,
62
+ RuntimeBroker,
63
+ RuntimeLink,
64
+ RuntimeLinkStore,
65
+ RuntimeRoute,
66
+ TransportContext,
67
+ )
68
+
69
+ __all__ = [
70
+ "RuntimeBrokerDeps",
71
+ "RuntimeBrokerRuntime",
72
+ "create_runtime_broker",
73
+ "runtime_source_key",
74
+ ]
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Resume-token reuse key
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ def runtime_source_key(source: str, model_id: str, bridge: str) -> str:
83
+ """Compose the composite reuse key a broker matches a persisted
84
+ :class:`~induscode.runtime_bridge.contract.RuntimeLink` against (TS
85
+ ``runtimeSourceKey``). A single ``source|model|bridge`` string rather
86
+ than three field comparisons — a stored link is reattachable iff its key
87
+ equals the key of the exchange about to run.
88
+
89
+ :param source: the model provider slug
90
+ :param model_id: the model id
91
+ :param bridge: the bridge adapter id
92
+ """
93
+ return f"{source}|{model_id}|{bridge}"
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Broker dependencies
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class RuntimeBrokerDeps:
103
+ """Construction-time dependencies for :func:`create_runtime_broker` (TS
104
+ ``RuntimeBrokerDeps``). All optional — an empty broker registers bridges
105
+ later, routes everything to the framework until a transport factory is
106
+ wired, and skips persistence when no store is set. Field names keep the
107
+ TS spelling."""
108
+
109
+ #: The seam that builds a ``ChildTransport`` for an external exchange.
110
+ #: When absent, an external route still *decides* ``"external"`` but
111
+ #: :meth:`RuntimeBrokerRuntime.exchange` cannot drive a child — so it
112
+ #: falls through to the framework path. Tests inject a fake-transport
113
+ #: factory here.
114
+ transportFactory: ChildTransportFactory | None = None
115
+ #: Where resume tokens persist; omitted ⇒ tokens are not persisted.
116
+ linkStore: RuntimeLinkStore | None = None
117
+ #: Override for the framework network stream (defaults to
118
+ #: ``stream_simple``). Injected in tests so the framework path is
119
+ #: observable without a network.
120
+ frameworkStream: FrameworkStream | None = None
121
+ #: Bridges to pre-register at construction (equivalent to ``register``).
122
+ bridges: Sequence[RuntimeBridge] = ()
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # The runtime broker
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ class RuntimeBrokerRuntime(RuntimeBroker, Protocol):
131
+ """The broker the product drives (TS ``RuntimeBrokerRuntime``). Extends
132
+ the frozen :class:`~induscode.runtime_bridge.contract.RuntimeBroker`
133
+ routing surface with :meth:`exchange`: the dispatch half that acts on a
134
+ :meth:`~induscode.runtime_bridge.contract.RuntimeBroker.route` decision
135
+ and returns the framework stream the turn streams into."""
136
+
137
+ def exchange(
138
+ self,
139
+ model: Model,
140
+ context: Context,
141
+ opts: ExchangeOptions | None = None,
142
+ ) -> AssistantMessageEventStream:
143
+ """Produce the turn for ``model``. Routes (via ``route``); on an
144
+ ``"external"`` route with a wired transport factory it drives the
145
+ bridge's ``run_exchange`` over a freshly-built ``ChildTransport``
146
+ (resolving + later persisting the resume token); otherwise it runs
147
+ the framework stream. Returns the ``AssistantMessageEventStream``
148
+ synchronously, like ``stream_simple``.
149
+
150
+ :param model: the model the turn is bound to
151
+ :param context: the framework conversation context
152
+ :param opts: per-exchange options
153
+ """
154
+ ...
155
+
156
+
157
+ class _Broker:
158
+ """The concrete broker. Private to the module; consumers obtain one
159
+ through :func:`create_runtime_broker`."""
160
+
161
+ __slots__ = ("_bridges", "_framework", "_link_store", "_transport_factory")
162
+
163
+ def __init__(self, deps: RuntimeBrokerDeps | None = None) -> None:
164
+ deps = deps if deps is not None else RuntimeBrokerDeps()
165
+ #: Registered bridges, keyed by adapter id (last registration wins).
166
+ self._bridges: dict[str, RuntimeBridge] = {}
167
+ self._transport_factory = deps.transportFactory
168
+ self._link_store = deps.linkStore
169
+ self._framework: FrameworkStream = (
170
+ deps.frameworkStream
171
+ if deps.frameworkStream is not None
172
+ else (lambda model, context, opts: stream_simple(model, context, opts))
173
+ )
174
+ for bridge in deps.bridges:
175
+ self.register(bridge)
176
+
177
+ # ---- registry ----
178
+
179
+ def register(self, bridge: RuntimeBridge) -> None:
180
+ self._bridges[bridge.adapter] = bridge
181
+
182
+ # ---- spec / credential resolution ----
183
+
184
+ def resolve_spec(self, model: Model) -> ExternalRuntimeSpec | None:
185
+ return spec_from_model(model)
186
+
187
+ def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
188
+ # Delegate to the owning bridge when registered; else fall back to the
189
+ # spec's own auth mode so the predicate answers without a bridge present.
190
+ bridge = self._bridges.get(spec.adapter)
191
+ if bridge is not None:
192
+ return bridge.requires_credential(spec)
193
+ return spec.authMode == "api-key"
194
+
195
+ # ---- routing ----
196
+
197
+ def route(self, model: Model, _context: Context, _opts: ExchangeOptions) -> RuntimeRoute:
198
+ spec = self.resolve_spec(model)
199
+ if spec is None:
200
+ return FrameworkRoute()
201
+ bridge = self._bridges.get(spec.adapter)
202
+ if bridge is None:
203
+ return FrameworkRoute()
204
+ return ExternalRoute(bridge=bridge, spec=spec)
205
+
206
+ # ---- dispatch ----
207
+
208
+ def exchange(
209
+ self,
210
+ model: Model,
211
+ context: Context,
212
+ opts: ExchangeOptions | None = None,
213
+ ) -> AssistantMessageEventStream:
214
+ opts = opts if opts is not None else ExchangeOptions()
215
+ route = self.route(model, context, opts)
216
+
217
+ # No external runtime, or no transport seam to drive a child: the
218
+ # framework network path produces the turn unchanged.
219
+ if route.target == "framework" or self._transport_factory is None:
220
+ return self._framework(model, context, opts)
221
+
222
+ bridge, spec = route.bridge, route.spec
223
+ source = str(_model_field(model, "provider"))
224
+ model_id = str(_model_field(model, "id"))
225
+ source_key = runtime_source_key(source, model_id, spec.adapter)
226
+
227
+ # Resolve a persisted resume token for this reuse key.
228
+ #
229
+ # # parity: SYNC FAST-PATH (TS `typeof persisted === "string"`) — only
230
+ # a *synchronously-available* resume token reattaches this dispatch.
231
+ # An async `find` (a coroutine/awaitable result) simply means no
232
+ # reattach on this first dispatch: the TS broker never awaits the
233
+ # promise, and this port preserves that quirk verbatim (the awaitable
234
+ # is closed, never awaited). A disk-backed store that wants reattach
235
+ # must answer `find` synchronously.
236
+ persisted = self._link_store.find(source_key) if self._link_store is not None else None
237
+ if inspect.iscoroutine(persisted):
238
+ persisted.close() # drop, don't await — see the parity note above
239
+ resume = persisted if isinstance(persisted, str) else opts.resume
240
+ exchange_opts = replace(opts, resume=resume) if resume is not None else opts
241
+
242
+ transport = self._transport_factory(
243
+ TransportContext(spec=spec, model=model, opts=exchange_opts, resume=resume)
244
+ )
245
+
246
+ # Tap inbound resume tokens off the transport so they persist as the
247
+ # renamed link entry. The tap is independent of the bridge's own
248
+ # parsing — both read the same messages; the bridge maps them to the
249
+ # event stream, this records the reattach token. Best-effort: a
250
+ # persistence failure never breaks a turn.
251
+ tap = _make_resume_tap(self._link_store, source, model_id, spec.adapter)
252
+ if tap is not None:
253
+ dispose = transport.on_message(tap.on_message)
254
+ tap.on_close(dispose)
255
+
256
+ return bridge.run_exchange(model, context, exchange_opts, transport)
257
+
258
+
259
+ def _model_field(model: Any, key: str) -> Any:
260
+ """Tolerant model-field read (dataclass attribute or mapping key)."""
261
+ try:
262
+ return getattr(model, key)
263
+ except AttributeError:
264
+ return model.get(key) if hasattr(model, "get") else None
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # Resume tap
269
+ # ---------------------------------------------------------------------------
270
+
271
+ #: Live fire-and-forget persistence tasks (kept referenced; pruned on settle).
272
+ _PENDING_SAVES: set[asyncio.Task[None]] = set()
273
+
274
+
275
+ async def _swallow(awaitable: Any) -> None:
276
+ try:
277
+ await awaitable
278
+ except Exception:
279
+ # best-effort: a persist failure must not break the turn.
280
+ pass
281
+
282
+
283
+ def _settle_quietly(result: Any) -> None:
284
+ """Settle an (optionally) async ``save`` result without ever letting it
285
+ break the turn — the Python analogue of the TS ``r.catch(() => {})``."""
286
+ if not inspect.isawaitable(result):
287
+ return
288
+ try:
289
+ loop = asyncio.get_running_loop()
290
+ except RuntimeError:
291
+ if inspect.iscoroutine(result):
292
+ result.close()
293
+ return
294
+ task = loop.create_task(_swallow(result))
295
+ _PENDING_SAVES.add(task)
296
+ task.add_done_callback(_PENDING_SAVES.discard)
297
+
298
+
299
+ def _make_resume_tap(
300
+ store: RuntimeLinkStore | None,
301
+ source: str,
302
+ model_id: str,
303
+ bridge: str,
304
+ ) -> Any | None:
305
+ """Build a best-effort resume-token tap that persists a
306
+ :class:`~induscode.runtime_bridge.contract.RuntimeLink` when a child
307
+ message carries one (TS ``makeResumeTap``). Returns ``None`` when no
308
+ store is wired (nothing to persist). The tap recognizes the
309
+ cross-dialect token shapes the bridges also surface as a ``resume``
310
+ normalized event: a CLI ``session_id`` (Anthropic ``system/init``), a
311
+ ``thread_id`` (OpenAI ``thread.started``), and an explicit
312
+ ``resumeToken`` (peer ``session/resume``). It is intentionally
313
+ permissive — it only records a token; misreads cost nothing because
314
+ reattach is keyed on :func:`runtime_source_key` and a stale token simply
315
+ yields a fresh session."""
316
+ if store is None:
317
+ return None
318
+ saved = False
319
+ disposer: Callable[[], None] | None = None
320
+
321
+ def persist(resume_token: str) -> None:
322
+ nonlocal saved
323
+ if saved:
324
+ return
325
+ saved = True
326
+ link = RuntimeLink(
327
+ source=source,
328
+ bridge=bridge,
329
+ resumeToken=resume_token,
330
+ at=datetime.now(timezone.utc).isoformat(),
331
+ )
332
+ try:
333
+ _settle_quietly(store.save(link))
334
+ except Exception:
335
+ # best-effort
336
+ pass
337
+ # One token per exchange is enough; drop the subscription once captured.
338
+ if disposer is not None:
339
+ disposer()
340
+
341
+ def on_message(message: Any) -> None:
342
+ token = _resume_token_of(message.payload)
343
+ if token is not None:
344
+ persist(token)
345
+
346
+ def on_close(dispose: Callable[[], None]) -> None:
347
+ nonlocal disposer
348
+ disposer = dispose
349
+
350
+ return SimpleNamespace(on_message=on_message, on_close=on_close)
351
+
352
+
353
+ def _resume_token_of(payload: Any) -> str | None:
354
+ """Extract a resume token from an opaque child payload across the wire
355
+ dialects (TS ``resumeTokenOf``)."""
356
+ if not isinstance(payload, dict):
357
+ return None
358
+
359
+ # Peer JSON-RPC: { method: "session/resume", params: { resumeToken } }.
360
+ if payload.get("method") == "session/resume":
361
+ params = payload.get("params")
362
+ if isinstance(params, dict):
363
+ token = params.get("resumeToken")
364
+ if isinstance(token, str):
365
+ return token
366
+ return None
367
+
368
+ # Anthropic stream-json: { type: "system", subtype: "init", session_id }.
369
+ if payload.get("type") == "system" and payload.get("subtype") == "init":
370
+ session_id = payload.get("session_id")
371
+ if isinstance(session_id, str):
372
+ return session_id
373
+
374
+ # OpenAI --json: { type: "thread.started", thread_id }.
375
+ if payload.get("type") == "thread.started":
376
+ thread_id = payload.get("thread_id")
377
+ if isinstance(thread_id, str):
378
+ return thread_id
379
+
380
+ return None
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # Factory
385
+ # ---------------------------------------------------------------------------
386
+
387
+
388
+ def create_runtime_broker(deps: RuntimeBrokerDeps | None = None) -> RuntimeBrokerRuntime:
389
+ """Construct a :class:`RuntimeBrokerRuntime` (TS ``createRuntimeBroker``).
390
+ The single sanctioned way to obtain a broker: build one with optional
391
+ dependencies (a transport factory, a resume link store, a
392
+ framework-stream override, pre-registered bridges), then ``register``
393
+ further bridges and drive turns through ``exchange`` / ``route``.
394
+
395
+ :param deps: optional construction-time dependencies
396
+ """
397
+ return _Broker(deps)