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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- 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)
|