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,734 @@
|
|
|
1
|
+
"""Runtime-bridge contract — the FROZEN type surface of provider *routing*
|
|
2
|
+
(port of TS ``src/runtime-bridge/contract.ts`` + the type half of
|
|
3
|
+
``broker.ts``).
|
|
4
|
+
|
|
5
|
+
This module is the single typed seam that decides, per model, **where a turn
|
|
6
|
+
is actually produced**: by the framework's own network stream
|
|
7
|
+
(:func:`indusagi.ai.stream_simple` over an HTTP provider) or by an *external
|
|
8
|
+
runtime* — a child coding-agent process driven over its own protocol (an
|
|
9
|
+
Anthropic-flavoured CLI speaking line-delimited JSON, an OpenAI-flavoured CLI
|
|
10
|
+
emitting ``--json`` items, or a peer agent reachable over JSON-RPC). The
|
|
11
|
+
product never calls a bridge or the framework stream directly; it asks the
|
|
12
|
+
:class:`RuntimeBroker` to route, and the broker picks the path.
|
|
13
|
+
|
|
14
|
+
Design stance (unchanged from the TS source):
|
|
15
|
+
|
|
16
|
+
- A model is *annotated*, not re-catalogued. The model catalog/matcher own
|
|
17
|
+
the model list; this layer only attaches an optional
|
|
18
|
+
:class:`ExternalRuntimeSpec` that says "this model is backed by a spawned
|
|
19
|
+
CLI / a peer agent, here is how to reach it and authenticate". A model
|
|
20
|
+
with no spec routes normally.
|
|
21
|
+
- Every external runtime speaks a *different* wire dialect, but the broker
|
|
22
|
+
should not care. Each bridge is reduced to a **parser** that yields a
|
|
23
|
+
provider-neutral :data:`NormalizedEvent` stream, and the single
|
|
24
|
+
:class:`BridgeEventSink` translates those events into the framework's
|
|
25
|
+
:class:`~indusagi.ai.AssistantMessageEventStream` push shape. The
|
|
26
|
+
imperative ``push_start/push_text_delta/push_done`` idiom lives in exactly
|
|
27
|
+
one place (:mod:`induscode.runtime_bridge.sink`).
|
|
28
|
+
- The child process / RPC peer a bridge drives is reached through an
|
|
29
|
+
**injectable** :class:`ChildTransport`, never a hard-coded spawn. Tests
|
|
30
|
+
hand the bridge a fake transport so no real ``claude``/``codex`` binary is
|
|
31
|
+
launched.
|
|
32
|
+
- Authentication policy is data, not control flow:
|
|
33
|
+
:meth:`RuntimeBridge.requires_credential` answers "does this model need a
|
|
34
|
+
key on disk before it can be offered?" — a spawned-CLI runtime that owns
|
|
35
|
+
its own auth returns ``False``, letting the model appear available with no
|
|
36
|
+
key.
|
|
37
|
+
|
|
38
|
+
Framework anchors (all from the sibling rebuilt framework, the ``indusagi``
|
|
39
|
+
package) — consumed verbatim, never re-derived: ``Model``, ``Context``,
|
|
40
|
+
``AssistantMessage``, ``AssistantMessageEventStream``, ``Api``,
|
|
41
|
+
``StopReason``, ``ToolCall``, ``SimpleStreamOptions``, ``KnownProvider`` from
|
|
42
|
+
:mod:`indusagi.ai`; ``stream_simple`` is the network fallback the broker
|
|
43
|
+
routes to.
|
|
44
|
+
|
|
45
|
+
Interface-dictated shapes preserved as-is: the ``bridge:<adapter>`` synthetic
|
|
46
|
+
endpoint convention for runtime-backed models, and the external CLI protocol
|
|
47
|
+
vocabularies — surfaced only as the opaque payloads a :class:`ChildTransport`
|
|
48
|
+
carries, never re-expressed.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
54
|
+
from dataclasses import dataclass, field
|
|
55
|
+
from typing import Any, ClassVar, Final, Literal, Protocol, TypeAlias
|
|
56
|
+
|
|
57
|
+
# Re-exported framework vocabulary routing consumers routinely compose.
|
|
58
|
+
from indusagi.ai import (
|
|
59
|
+
Api,
|
|
60
|
+
AssistantMessage,
|
|
61
|
+
AssistantMessageEventStream,
|
|
62
|
+
Context,
|
|
63
|
+
KnownProvider,
|
|
64
|
+
Model,
|
|
65
|
+
SimpleStreamOptions,
|
|
66
|
+
StopReason,
|
|
67
|
+
ToolCall,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"Api",
|
|
72
|
+
"AssistantMessage",
|
|
73
|
+
"AssistantMessageEventStream",
|
|
74
|
+
"BridgeEventSink",
|
|
75
|
+
"BridgeFailure",
|
|
76
|
+
"ChildMessage",
|
|
77
|
+
"ChildRequest",
|
|
78
|
+
"ChildTransport",
|
|
79
|
+
"ChildTransportFactory",
|
|
80
|
+
"Context",
|
|
81
|
+
"ExchangeOptions",
|
|
82
|
+
"ExternalRoute",
|
|
83
|
+
"ExternalRuntimeSpec",
|
|
84
|
+
"FailedEvent",
|
|
85
|
+
"FinishEvent",
|
|
86
|
+
"FinishReason",
|
|
87
|
+
"FrameworkRoute",
|
|
88
|
+
"FrameworkStream",
|
|
89
|
+
"KnownProvider",
|
|
90
|
+
"Model",
|
|
91
|
+
"NORMALIZED_EVENT_KINDS",
|
|
92
|
+
"NormalizedEvent",
|
|
93
|
+
"NormalizedEventKind",
|
|
94
|
+
"RUNTIME_ENDPOINT_SCHEME",
|
|
95
|
+
"RUNTIME_LINK_ENTRY",
|
|
96
|
+
"ResumeEvent",
|
|
97
|
+
"RuntimeAdapterId",
|
|
98
|
+
"RuntimeAuthMode",
|
|
99
|
+
"RuntimeBridge",
|
|
100
|
+
"RuntimeBroker",
|
|
101
|
+
"RuntimeEndpointScheme",
|
|
102
|
+
"RuntimeLink",
|
|
103
|
+
"RuntimeLinkEntryTag",
|
|
104
|
+
"RuntimeLinkStore",
|
|
105
|
+
"RuntimeRoute",
|
|
106
|
+
"SimpleStreamOptions",
|
|
107
|
+
"StopReason",
|
|
108
|
+
"TextEvent",
|
|
109
|
+
"ThinkingEvent",
|
|
110
|
+
"ToolCall",
|
|
111
|
+
"ToolCallEvent",
|
|
112
|
+
"TransportContext",
|
|
113
|
+
"runtime_endpoint",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# External-runtime annotation
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
#: The set of bridge adapters this layer ships. Each value names a concrete
|
|
122
|
+
#: :class:`RuntimeBridge` implementation and the wire dialect it speaks:
|
|
123
|
+
#:
|
|
124
|
+
#: - ``"claude-cli"`` — drives an Anthropic-flavoured CLI emitting
|
|
125
|
+
#: line-delimited stream-json content blocks.
|
|
126
|
+
#: - ``"codex-cli"`` — drives an OpenAI-flavoured CLI emitting ``--json``
|
|
127
|
+
#: turn/item events.
|
|
128
|
+
#: - ``"indusagi-cli"`` — drives a peer agent over JSON-RPC; the child
|
|
129
|
+
#: already speaks the framework event vocabulary, so its events map onto
|
|
130
|
+
#: :data:`NormalizedEvent` near-directly.
|
|
131
|
+
#:
|
|
132
|
+
#: Left open (TS ``(string & {})``) so an extension can register a further
|
|
133
|
+
#: adapter without editing this alias, while the three shipped ids live in
|
|
134
|
+
#: ``bridges.BUILTIN_ADAPTERS``.
|
|
135
|
+
RuntimeAdapterId: TypeAlias = str
|
|
136
|
+
|
|
137
|
+
#: How a model bound to an external runtime authenticates.
|
|
138
|
+
#:
|
|
139
|
+
#: - ``"external-cli"`` — the spawned child owns its own auth (its own login
|
|
140
|
+
#: / keychain); the product needs **no** key on disk, so the model can be
|
|
141
|
+
#: offered as available with an empty vault. This is the policy that makes
|
|
142
|
+
#: a CLI-backed model "just work" once the underlying tool is logged in.
|
|
143
|
+
#: - ``"api-key"`` — the runtime still needs an API key resolved from the
|
|
144
|
+
#: credential vault, same as a normal HTTP provider; only the *transport*
|
|
145
|
+
#: differs.
|
|
146
|
+
RuntimeAuthMode: TypeAlias = Literal["external-cli", "api-key"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class ExternalRuntimeSpec:
|
|
151
|
+
"""The annotation that turns an ordinary framework ``Model`` into an
|
|
152
|
+
external-runtime model (TS ``ExternalRuntimeSpec``).
|
|
153
|
+
|
|
154
|
+
Attached alongside a catalog card (it is *additive* metadata — the
|
|
155
|
+
catalog/matcher are unaware of it) and consulted by the
|
|
156
|
+
:class:`RuntimeBroker` to decide routing. All transport fields are
|
|
157
|
+
optional: a bridge may have a sensible default binary, take no extra
|
|
158
|
+
args, inherit the parent environment, and route to its own downstream
|
|
159
|
+
source. Only ``adapter`` is required, because it selects which
|
|
160
|
+
:class:`RuntimeBridge` owns the exchange. Field names keep the TS
|
|
161
|
+
spelling.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
#: Which bridge owns this model's exchange (the :attr:`RuntimeBridge.adapter`).
|
|
165
|
+
adapter: RuntimeAdapterId
|
|
166
|
+
#: Whether the runtime needs a key on disk, or owns its own auth.
|
|
167
|
+
authMode: RuntimeAuthMode
|
|
168
|
+
#: Override for the child executable to launch (defaults to the bridge's own).
|
|
169
|
+
binaryPath: str | None = None
|
|
170
|
+
#: Extra command-line arguments prepended to the bridge's protocol flags.
|
|
171
|
+
args: tuple[str, ...] | None = None
|
|
172
|
+
#: Environment overrides merged into the child process environment.
|
|
173
|
+
env: Mapping[str, str] | None = None
|
|
174
|
+
#: For a bridge that fronts another source (e.g. a peer agent that itself
|
|
175
|
+
#: talks to a downstream provider), the provider slug the child should
|
|
176
|
+
#: target. Ignored by bridges that terminate the exchange themselves.
|
|
177
|
+
delegate: str | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
#: The synthetic endpoint scheme stamped onto a runtime-backed model's
|
|
181
|
+
#: ``baseUrl``. A model annotated with :class:`ExternalRuntimeSpec` carries
|
|
182
|
+
#: ``baseUrl == f"{RUNTIME_ENDPOINT_SCHEME}{adapter}"`` so it has a stable,
|
|
183
|
+
#: non-HTTP address that never resolves to a network host.
|
|
184
|
+
RUNTIME_ENDPOINT_SCHEME: Final = "bridge:"
|
|
185
|
+
|
|
186
|
+
#: The literal type of :data:`RUNTIME_ENDPOINT_SCHEME`.
|
|
187
|
+
RuntimeEndpointScheme: TypeAlias = Literal["bridge:"]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def runtime_endpoint(adapter: RuntimeAdapterId) -> str:
|
|
191
|
+
"""Compose the synthetic ``bridge:<adapter>`` endpoint for a
|
|
192
|
+
runtime-backed model. Inert string helper; the single sanctioned way to
|
|
193
|
+
mint the convention so it stays uniform across the catalog annotation
|
|
194
|
+
and the broker's routing check.
|
|
195
|
+
|
|
196
|
+
:param adapter: the :attr:`RuntimeBridge.adapter` owning the model
|
|
197
|
+
"""
|
|
198
|
+
return f"{RUNTIME_ENDPOINT_SCHEME}{adapter}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Normalized, provider-neutral event
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
#
|
|
205
|
+
# A provider-neutral streamed event — the common currency between a bridge's
|
|
206
|
+
# per-dialect parser and the BridgeEventSink. Each external runtime emits its
|
|
207
|
+
# own wire vocabulary (Anthropic content blocks, OpenAI items, peer JSON-RPC
|
|
208
|
+
# events). A bridge's only job is to map that vocabulary onto this small,
|
|
209
|
+
# closed union; the sink then translates a NormalizedEvent into the matching
|
|
210
|
+
# framework AssistantMessageEventStream push call. This is the seam that
|
|
211
|
+
# removes the per-bridge `stream.push*` duplication.
|
|
212
|
+
|
|
213
|
+
#: The terminal reasons an external exchange can settle on. A subset of the
|
|
214
|
+
#: framework's ``StopReason`` — the non-error outcomes a bridge reports to
|
|
215
|
+
#: the sink, which then emits the framework ``done`` event.
|
|
216
|
+
FinishReason: TypeAlias = Literal["stop", "length", "toolUse"]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True, slots=True)
|
|
220
|
+
class BridgeFailure:
|
|
221
|
+
"""A typed failure surfaced by a bridge when an external exchange breaks
|
|
222
|
+
(TS ``BridgeFailure``). The ``aborted`` flag distinguishes a
|
|
223
|
+
caller-cancelled exchange (the child was interrupted) from a genuine
|
|
224
|
+
fault, so the sink can emit the matching framework error reason
|
|
225
|
+
(``aborted`` vs ``error``)."""
|
|
226
|
+
|
|
227
|
+
#: Human-readable, single-line summary of what went wrong.
|
|
228
|
+
message: str
|
|
229
|
+
#: True when the exchange was cancelled rather than failing on its own.
|
|
230
|
+
aborted: bool = False
|
|
231
|
+
#: Underlying error or structured detail, if any.
|
|
232
|
+
cause: Any = None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True, slots=True)
|
|
236
|
+
class TextEvent:
|
|
237
|
+
"""A chunk of assistant answer text."""
|
|
238
|
+
|
|
239
|
+
kind: ClassVar[Literal["text"]] = "text"
|
|
240
|
+
delta: str
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class ThinkingEvent:
|
|
245
|
+
"""A chunk of reasoning text (runtimes without a thinking channel simply
|
|
246
|
+
never emit this)."""
|
|
247
|
+
|
|
248
|
+
kind: ClassVar[Literal["thinking"]] = "thinking"
|
|
249
|
+
delta: str
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True, slots=True)
|
|
253
|
+
class ToolCallEvent:
|
|
254
|
+
"""A fully-formed tool invocation the child decided on."""
|
|
255
|
+
|
|
256
|
+
kind: ClassVar[Literal["tool_call"]] = "tool_call"
|
|
257
|
+
call: ToolCall
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True, slots=True)
|
|
261
|
+
class ResumeEvent:
|
|
262
|
+
"""The child reported a session/thread id the bridge can persist to
|
|
263
|
+
reattach the underlying CLI session after a restart. Informational on the
|
|
264
|
+
stream (the sink ignores it); the broker persists it out of band."""
|
|
265
|
+
|
|
266
|
+
kind: ClassVar[Literal["resume"]] = "resume"
|
|
267
|
+
resumeToken: str
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True, slots=True)
|
|
271
|
+
class FinishEvent:
|
|
272
|
+
"""The exchange settled successfully with a terminal reason."""
|
|
273
|
+
|
|
274
|
+
kind: ClassVar[Literal["finish"]] = "finish"
|
|
275
|
+
reason: FinishReason
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True, slots=True)
|
|
279
|
+
class FailedEvent:
|
|
280
|
+
"""The exchange ended in error."""
|
|
281
|
+
|
|
282
|
+
kind: ClassVar[Literal["failed"]] = "failed"
|
|
283
|
+
error: BridgeFailure
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
#: The provider-neutral event union (TS ``NormalizedEvent``).
|
|
287
|
+
NormalizedEvent: TypeAlias = (
|
|
288
|
+
TextEvent | ThinkingEvent | ToolCallEvent | ResumeEvent | FinishEvent | FailedEvent
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
#: The discriminant literals of :data:`NormalizedEvent`, for filtering/logging.
|
|
292
|
+
NormalizedEventKind: TypeAlias = Literal[
|
|
293
|
+
"text", "thinking", "tool_call", "resume", "finish", "failed"
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
#: Every :data:`NormalizedEventKind` value, as a frozen tuple. The sink's
|
|
297
|
+
#: dispatch table and the kind-coverage test both pin against this (the
|
|
298
|
+
#: Python replacement for the TS mapped-type exhaustiveness).
|
|
299
|
+
NORMALIZED_EVENT_KINDS: Final[tuple[NormalizedEventKind, ...]] = (
|
|
300
|
+
"text",
|
|
301
|
+
"thinking",
|
|
302
|
+
"tool_call",
|
|
303
|
+
"resume",
|
|
304
|
+
"finish",
|
|
305
|
+
"failed",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Bridge event sink
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class BridgeEventSink(Protocol):
|
|
315
|
+
"""The single push-stream helper every bridge writes through (TS
|
|
316
|
+
``BridgeEventSink``).
|
|
317
|
+
|
|
318
|
+
It owns the accumulating ``AssistantMessage``, the content-block index
|
|
319
|
+
bookkeeping, and the lazily-started lifecycle (the first emission opens
|
|
320
|
+
the stream). A bridge never touches ``AssistantMessageEventStream``
|
|
321
|
+
directly; it constructs a sink, drives it with normalized events, and
|
|
322
|
+
returns the sink's :attr:`stream`. This centralizes the
|
|
323
|
+
``start → push* → finish_success/finish_error`` idiom so the per-bridge
|
|
324
|
+
code is purely a parser. The convenience methods cover the common path;
|
|
325
|
+
:meth:`emit` accepts a raw :data:`NormalizedEvent` for parsers that
|
|
326
|
+
prefer to yield the union directly.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def start(self) -> None:
|
|
330
|
+
"""Open the underlying stream and push the framework ``start`` event,
|
|
331
|
+
if it has not already started. Idempotent: safe to call before any
|
|
332
|
+
emission, and a no-op once started. The convenience emitters call it
|
|
333
|
+
implicitly."""
|
|
334
|
+
...
|
|
335
|
+
|
|
336
|
+
def text(self, delta: str) -> None:
|
|
337
|
+
"""Append a chunk of assistant answer text (opens a text block on
|
|
338
|
+
first call)."""
|
|
339
|
+
...
|
|
340
|
+
|
|
341
|
+
def thinking(self, delta: str) -> None:
|
|
342
|
+
"""Append a chunk of reasoning text (opens a thinking block on first
|
|
343
|
+
call)."""
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
def tool_call(self, call: ToolCall) -> None:
|
|
347
|
+
"""Emit a fully-formed tool call as its own content block."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
def emit(self, event: NormalizedEvent) -> None:
|
|
351
|
+
"""Map one :data:`NormalizedEvent` onto the matching push call. The
|
|
352
|
+
single entry point a parser can drive with the raw union; dispatches
|
|
353
|
+
to :meth:`text` / :meth:`thinking` / :meth:`tool_call` /
|
|
354
|
+
:meth:`finish_success` / :meth:`finish_error` by ``kind``. A
|
|
355
|
+
``resume`` event is informational and does not touch the stream."""
|
|
356
|
+
...
|
|
357
|
+
|
|
358
|
+
def finish_success(self, reason: FinishReason = "stop") -> None:
|
|
359
|
+
"""Settle the exchange successfully: close any open block, finalize
|
|
360
|
+
the accumulated message, and push the framework ``done`` event with
|
|
361
|
+
``reason``. Terminal — no further emissions are valid after this.
|
|
362
|
+
|
|
363
|
+
:param reason: the terminal stop reason (defaults to ``"stop"``)
|
|
364
|
+
"""
|
|
365
|
+
...
|
|
366
|
+
|
|
367
|
+
def finish_error(self, error: BridgeFailure) -> None:
|
|
368
|
+
"""Settle the exchange in error: push the framework ``error`` event.
|
|
369
|
+
The :attr:`BridgeFailure.aborted` flag selects the framework error
|
|
370
|
+
reason (``aborted`` vs ``error``). Terminal.
|
|
371
|
+
|
|
372
|
+
:param error: the typed bridge failure
|
|
373
|
+
"""
|
|
374
|
+
...
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def stream(self) -> AssistantMessageEventStream:
|
|
378
|
+
"""The framework push stream the broker hands back to its caller.
|
|
379
|
+
Populated asynchronously as the bridge drives the sink; consumers
|
|
380
|
+
iterate it exactly as they would the stream ``stream_simple``
|
|
381
|
+
returns."""
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# Injectable child transport
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@dataclass(frozen=True, slots=True)
|
|
391
|
+
class ChildMessage:
|
|
392
|
+
"""A single message exchanged with the child runtime over its
|
|
393
|
+
:class:`ChildTransport` (TS ``ChildMessage``).
|
|
394
|
+
|
|
395
|
+
The ``payload`` is deliberately opaque: it is whatever the underlying
|
|
396
|
+
protocol carries — a parsed JSON line from a CLI's NDJSON stdout, a
|
|
397
|
+
JSON-RPC notification from a peer agent, a raw text chunk. The bridge
|
|
398
|
+
that owns the transport knows how to interpret it; the contract only
|
|
399
|
+
fixes the envelope so the transport interface itself is dialect-agnostic
|
|
400
|
+
and mockable.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
#: The protocol payload (a parsed JSON value, a text line, an RPC frame).
|
|
404
|
+
payload: Any
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@dataclass(frozen=True, slots=True)
|
|
408
|
+
class ChildRequest:
|
|
409
|
+
"""A request sent *to* the child runtime (TS ``ChildRequest``). The
|
|
410
|
+
bridge formats the dialect-specific body; the transport only relays it.
|
|
411
|
+
``body`` is opaque for the same reason :attr:`ChildMessage.payload` is."""
|
|
412
|
+
|
|
413
|
+
#: The dialect-specific request body the bridge constructed.
|
|
414
|
+
body: Any
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class ChildTransport(Protocol):
|
|
418
|
+
"""The injectable boundary between a bridge and the actual child process
|
|
419
|
+
/ RPC peer it drives (TS ``ChildTransport``).
|
|
420
|
+
|
|
421
|
+
A production transport wraps a spawned process (its stdin/stdout, or a
|
|
422
|
+
JSON-RPC client over that process); a test transport is a hand-written
|
|
423
|
+
fake that records ``send``\\ s and replays canned :class:`ChildMessage`\\ s
|
|
424
|
+
— so a bridge's parser can be exercised with **no real binary launched**.
|
|
425
|
+
The bridge depends only on this interface, never on a subprocess module
|
|
426
|
+
directly.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
def send(self, request: ChildRequest) -> Awaitable[None]:
|
|
430
|
+
"""Relay one :class:`ChildRequest` to the child (e.g. write a prompt
|
|
431
|
+
to stdin or issue a JSON-RPC call). Resolves once the request has
|
|
432
|
+
been handed off.
|
|
433
|
+
|
|
434
|
+
:param request: the dialect-specific request to deliver
|
|
435
|
+
"""
|
|
436
|
+
...
|
|
437
|
+
|
|
438
|
+
def on_message(self, listener: Callable[[ChildMessage], None]) -> Callable[[], None]:
|
|
439
|
+
"""Register a listener for inbound :class:`ChildMessage`\\ s from the
|
|
440
|
+
child (stdout lines, RPC notifications). Returns an unsubscribe
|
|
441
|
+
function.
|
|
442
|
+
|
|
443
|
+
:param listener: invoked for each inbound message
|
|
444
|
+
:returns: a disposer that removes the listener
|
|
445
|
+
"""
|
|
446
|
+
...
|
|
447
|
+
|
|
448
|
+
def close(self) -> Awaitable[None]:
|
|
449
|
+
"""Terminate the child and release the transport. Idempotent; safe to
|
|
450
|
+
call on an already-closed transport. After ``close``, ``send``
|
|
451
|
+
rejects and no further messages are delivered."""
|
|
452
|
+
...
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Runtime exchange options
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@dataclass
|
|
461
|
+
class ExchangeOptions(SimpleStreamOptions):
|
|
462
|
+
"""Per-exchange options threaded into :meth:`RuntimeBridge.run_exchange`
|
|
463
|
+
(and the framework ``stream_simple`` fallback the broker also drives)
|
|
464
|
+
(TS ``ExchangeOptions``).
|
|
465
|
+
|
|
466
|
+
Extends the framework's :class:`~indusagi.ai.SimpleStreamOptions` (so
|
|
467
|
+
``signal``, ``apiKey``, ``reasoning``, ``sessionId``, etc. flow straight
|
|
468
|
+
through to the network path) with the routing-layer extras a bridge
|
|
469
|
+
needs:
|
|
470
|
+
|
|
471
|
+
- ``sessionId`` (inherited) correlates the exchange with a persisted
|
|
472
|
+
runtime session so the bridge can resume the underlying CLI session.
|
|
473
|
+
- ``cwd`` is the working directory the child runtime is scoped to
|
|
474
|
+
(defaults to the process cwd).
|
|
475
|
+
- ``resume`` carries a previously-persisted token (e.g. a CLI session id
|
|
476
|
+
/ thread id) the bridge reattaches to instead of starting fresh.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
#: Working directory the child runtime is scoped to.
|
|
480
|
+
cwd: str | None = field(default=None, kw_only=True)
|
|
481
|
+
#: A resume token from a prior exchange to reattach the underlying session.
|
|
482
|
+
resume: str | None = field(default=None, kw_only=True)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ---------------------------------------------------------------------------
|
|
486
|
+
# Runtime bridge
|
|
487
|
+
# ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class RuntimeBridge(Protocol):
|
|
491
|
+
"""One external-runtime adapter: the strategy that produces a turn for a
|
|
492
|
+
model whose :class:`ExternalRuntimeSpec` names it (TS ``RuntimeBridge``).
|
|
493
|
+
|
|
494
|
+
A bridge is the dialect specialist. Given the bound model, the framework
|
|
495
|
+
context, per-exchange :class:`ExchangeOptions`, and an **injected**
|
|
496
|
+
:class:`ChildTransport`, it drives the child and returns the framework
|
|
497
|
+
push stream the turn streams into. The transport is a parameter (not
|
|
498
|
+
constructed internally) precisely so tests pass a fake and no real CLI
|
|
499
|
+
is spawned. Bridges hold no per-session state on the contract surface;
|
|
500
|
+
live session handles and reuse/persistence are the
|
|
501
|
+
:class:`RuntimeBroker`'s concern.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def adapter(self) -> RuntimeAdapterId:
|
|
506
|
+
"""The adapter id this bridge answers to (matched against
|
|
507
|
+
:attr:`ExternalRuntimeSpec.adapter`)."""
|
|
508
|
+
...
|
|
509
|
+
|
|
510
|
+
def run_exchange(
|
|
511
|
+
self,
|
|
512
|
+
model: Model,
|
|
513
|
+
context: Context,
|
|
514
|
+
opts: ExchangeOptions,
|
|
515
|
+
transport: ChildTransport,
|
|
516
|
+
) -> AssistantMessageEventStream:
|
|
517
|
+
"""Drive one exchange against the external runtime and return the
|
|
518
|
+
framework push stream it streams into. Returns the stream
|
|
519
|
+
**synchronously** (the stream is populated asynchronously as the
|
|
520
|
+
child emits), matching the shape of the framework's own
|
|
521
|
+
``stream_simple``. The implementation reads inbound
|
|
522
|
+
:class:`ChildMessage`\\ s off ``transport``, parses them into
|
|
523
|
+
:data:`NormalizedEvent`\\ s, and feeds a :class:`BridgeEventSink`.
|
|
524
|
+
|
|
525
|
+
:param model: the bound framework model for this exchange
|
|
526
|
+
:param context: the framework conversation context
|
|
527
|
+
:param opts: per-exchange options (session id, cwd, resume, stream opts)
|
|
528
|
+
:param transport: the injected child boundary to drive
|
|
529
|
+
"""
|
|
530
|
+
...
|
|
531
|
+
|
|
532
|
+
def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
|
|
533
|
+
"""Whether a model bound to this bridge needs a credential on disk
|
|
534
|
+
before it can be offered. Returns ``False`` for an ``"external-cli"``
|
|
535
|
+
spec whose child owns its own auth (so the model is available with an
|
|
536
|
+
empty vault), ``True`` for an ``"api-key"`` spec. The central
|
|
537
|
+
auth-routing predicate.
|
|
538
|
+
|
|
539
|
+
:param spec: the external-runtime annotation to evaluate
|
|
540
|
+
"""
|
|
541
|
+
...
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# Runtime broker
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@dataclass(frozen=True, slots=True)
|
|
550
|
+
class ExternalRoute:
|
|
551
|
+
"""An external runtime owns the exchange: carries the chosen
|
|
552
|
+
:class:`RuntimeBridge` and the resolved :class:`ExternalRuntimeSpec` the
|
|
553
|
+
broker will drive ``run_exchange`` with."""
|
|
554
|
+
|
|
555
|
+
target: ClassVar[Literal["external"]] = "external"
|
|
556
|
+
bridge: RuntimeBridge
|
|
557
|
+
spec: ExternalRuntimeSpec
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@dataclass(frozen=True, slots=True)
|
|
561
|
+
class FrameworkRoute:
|
|
562
|
+
"""The turn falls through to the framework network stream
|
|
563
|
+
(``stream_simple``) unchanged."""
|
|
564
|
+
|
|
565
|
+
target: ClassVar[Literal["framework"]] = "framework"
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
#: The outcome of a routing decision (TS ``RuntimeRoute``).
|
|
569
|
+
RuntimeRoute: TypeAlias = ExternalRoute | FrameworkRoute
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class RuntimeBroker(Protocol):
|
|
573
|
+
"""The router and registry over :class:`RuntimeBridge`\\ s — the single
|
|
574
|
+
decision point for "external runtime vs framework stream" (TS
|
|
575
|
+
``RuntimeBroker``).
|
|
576
|
+
|
|
577
|
+
The broker is asked to :meth:`route` every turn. If the model carries an
|
|
578
|
+
:class:`ExternalRuntimeSpec` whose adapter resolves to a registered
|
|
579
|
+
bridge, the broker returns an ``"external"`` route the caller drives via
|
|
580
|
+
the bridge's ``run_exchange``; otherwise it returns a ``"framework"``
|
|
581
|
+
route and the caller runs ``stream_simple``. Bridges are
|
|
582
|
+
:meth:`register`\\ ed at assembly time and looked up by adapter id.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def register(self, bridge: RuntimeBridge) -> None:
|
|
586
|
+
"""Add a :class:`RuntimeBridge` to the registry, keyed by its
|
|
587
|
+
:attr:`RuntimeBridge.adapter`. Registering an adapter id that already
|
|
588
|
+
exists replaces the prior bridge.
|
|
589
|
+
|
|
590
|
+
:param bridge: the bridge to register
|
|
591
|
+
"""
|
|
592
|
+
...
|
|
593
|
+
|
|
594
|
+
def route(self, model: Model, context: Context, opts: ExchangeOptions) -> RuntimeRoute:
|
|
595
|
+
"""Decide how to produce a turn for ``model``. Resolves the model's
|
|
596
|
+
:class:`ExternalRuntimeSpec` (via :meth:`resolve_spec`) and a matching
|
|
597
|
+
registered bridge; returns an ``"external"`` route when both are
|
|
598
|
+
present, else a ``"framework"`` route. The framework
|
|
599
|
+
``context``/``opts`` are accepted so an implementation may factor
|
|
600
|
+
them into routing, even though the base decision keys on the model.
|
|
601
|
+
|
|
602
|
+
:param model: the model the turn is bound to
|
|
603
|
+
:param context: the framework conversation context for the turn
|
|
604
|
+
:param opts: per-exchange options
|
|
605
|
+
"""
|
|
606
|
+
...
|
|
607
|
+
|
|
608
|
+
def resolve_spec(self, model: Model) -> ExternalRuntimeSpec | None:
|
|
609
|
+
"""Resolve the :class:`ExternalRuntimeSpec` annotated onto a model,
|
|
610
|
+
or ``None`` when the model is a plain HTTP-provider model. How the
|
|
611
|
+
spec is attached (a side-table keyed by canonical id, a field on a
|
|
612
|
+
catalog card, a ``bridge:<adapter>`` baseUrl decode) is the
|
|
613
|
+
implementation's choice; the contract only fixes the lookup.
|
|
614
|
+
|
|
615
|
+
:param model: the model to inspect for a runtime annotation
|
|
616
|
+
"""
|
|
617
|
+
...
|
|
618
|
+
|
|
619
|
+
def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
|
|
620
|
+
"""Whether a runtime-annotated model needs a credential on disk
|
|
621
|
+
before it can be offered as available. Delegates to the owning
|
|
622
|
+
bridge's :meth:`RuntimeBridge.requires_credential`; falls back to the
|
|
623
|
+
spec's own ``authMode`` when no bridge is registered — so this
|
|
624
|
+
answers strictly the *runtime* credential question.
|
|
625
|
+
|
|
626
|
+
:param spec: the runtime annotation to evaluate
|
|
627
|
+
"""
|
|
628
|
+
...
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ---------------------------------------------------------------------------
|
|
632
|
+
# Resume-token persistence (type half of TS broker.ts)
|
|
633
|
+
# ---------------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
#: The custom transcript-entry tag under which a runtime resume token is
|
|
636
|
+
#: logged. The record this build writes is its own ``"external-runtime-link"``
|
|
637
|
+
#: shape so the persisted log carries no inherited vocabulary. A consumer
|
|
638
|
+
#: scanning the active branch backwards for a reattachable session matches on
|
|
639
|
+
#: this tag.
|
|
640
|
+
RUNTIME_LINK_ENTRY: Final = "external-runtime-link"
|
|
641
|
+
|
|
642
|
+
#: The literal type of :data:`RUNTIME_LINK_ENTRY`.
|
|
643
|
+
RuntimeLinkEntryTag: TypeAlias = Literal["external-runtime-link"]
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@dataclass(frozen=True, slots=True)
|
|
647
|
+
class RuntimeLink:
|
|
648
|
+
"""The serializable payload persisted under :data:`RUNTIME_LINK_ENTRY`
|
|
649
|
+
(TS ``RuntimeLink``).
|
|
650
|
+
|
|
651
|
+
The renamed shape: ``{source, bridge, resumeToken, at}``. ``source`` is
|
|
652
|
+
the model's provider slug, ``bridge`` is the owning
|
|
653
|
+
:attr:`RuntimeBridge.adapter`, ``resumeToken`` is the reattachable CLI
|
|
654
|
+
session id / thread id the child reported, and ``at`` is the ISO instant
|
|
655
|
+
it was captured. Reuse keys on
|
|
656
|
+
:func:`~induscode.runtime_bridge.broker.runtime_source_key`, derived from
|
|
657
|
+
``source`` + model id + ``bridge``.
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
#: The model's provider slug (its ``source``).
|
|
661
|
+
source: str
|
|
662
|
+
#: The owning bridge adapter id.
|
|
663
|
+
bridge: str
|
|
664
|
+
#: The reattachable session id / thread id reported by the child runtime.
|
|
665
|
+
resumeToken: str
|
|
666
|
+
#: ISO-8601 instant the token was captured.
|
|
667
|
+
at: str
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class RuntimeLinkStore(Protocol):
|
|
671
|
+
"""Injectable persistence boundary for resume tokens (TS
|
|
672
|
+
``RuntimeLinkStore``). The conductor's transcript store binds a real
|
|
673
|
+
implementation (appending a :data:`RUNTIME_LINK_ENTRY` custom entry to
|
|
674
|
+
the active branch and scanning it backwards on lookup); tests pass an
|
|
675
|
+
in-memory fake. Both methods *may* be async to match a disk-backed
|
|
676
|
+
transcript — but see the sync-fast-path note on
|
|
677
|
+
:meth:`~induscode.runtime_bridge.broker._Broker.exchange`."""
|
|
678
|
+
|
|
679
|
+
def save(self, link: RuntimeLink) -> Awaitable[None] | None:
|
|
680
|
+
"""Persist a captured resume token as a renamed custom transcript
|
|
681
|
+
entry. Called once per ``resume`` event a bridge surfaces during an
|
|
682
|
+
exchange.
|
|
683
|
+
|
|
684
|
+
:param link: the renamed link record to append
|
|
685
|
+
"""
|
|
686
|
+
...
|
|
687
|
+
|
|
688
|
+
def find(self, source_key: str) -> Awaitable[str | None] | str | None:
|
|
689
|
+
"""Resolve the most recent reattachable token for a reuse key, or
|
|
690
|
+
``None`` when the active branch holds no matching
|
|
691
|
+
:data:`RUNTIME_LINK_ENTRY`. The key is
|
|
692
|
+
:func:`~induscode.runtime_bridge.broker.runtime_source_key`.
|
|
693
|
+
|
|
694
|
+
:param source_key: the composite ``source|model|bridge`` reuse key
|
|
695
|
+
"""
|
|
696
|
+
...
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
# ---------------------------------------------------------------------------
|
|
700
|
+
# Transport factory (type half of TS broker.ts)
|
|
701
|
+
# ---------------------------------------------------------------------------
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@dataclass(frozen=True, slots=True)
|
|
705
|
+
class TransportContext:
|
|
706
|
+
"""The context handed to a :data:`ChildTransportFactory` when the broker
|
|
707
|
+
needs a transport for an external exchange (TS ``TransportContext``).
|
|
708
|
+
Carries everything a production factory needs to launch + wire the child
|
|
709
|
+
(the resolved spec's binary/args/env, the working directory, a resume
|
|
710
|
+
token to reattach) without the broker itself touching a subprocess
|
|
711
|
+
module."""
|
|
712
|
+
|
|
713
|
+
#: The resolved runtime spec (binary, args, env, delegate) for the child.
|
|
714
|
+
spec: ExternalRuntimeSpec
|
|
715
|
+
#: The bound model the exchange runs for.
|
|
716
|
+
model: Model
|
|
717
|
+
#: The per-exchange options (session id, cwd, resume, stream opts).
|
|
718
|
+
opts: ExchangeOptions
|
|
719
|
+
#: A persisted resume token resolved for this exchange, if any.
|
|
720
|
+
resume: str | None = None
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
#: Mints a :class:`ChildTransport` for an external exchange (TS
|
|
724
|
+
#: ``ChildTransportFactory``). The single seam the broker reaches the outside
|
|
725
|
+
#: world through: a production factory spawns the process and adapts its
|
|
726
|
+
#: stdio/RPC into the transport; a test factory returns a scripted fake. The
|
|
727
|
+
#: broker calls it lazily, only on an external route.
|
|
728
|
+
ChildTransportFactory: TypeAlias = Callable[[TransportContext], ChildTransport]
|
|
729
|
+
|
|
730
|
+
#: The framework network-stream signature the broker falls through to (TS
|
|
731
|
+
#: ``FrameworkStream``).
|
|
732
|
+
FrameworkStream: TypeAlias = Callable[
|
|
733
|
+
[Model, Context, ExchangeOptions], AssistantMessageEventStream
|
|
734
|
+
]
|