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,166 @@
1
+ """Runtime-bridge subsystem — public barrel (port of TS
2
+ ``src/runtime-bridge/index.ts``).
3
+
4
+ Re-exports the FROZEN provider-routing contract: the external-runtime
5
+ annotation (:class:`ExternalRuntimeSpec` + the ``bridge:<adapter>`` endpoint
6
+ convention), the provider-neutral :data:`NormalizedEvent` union, the single
7
+ :class:`BridgeEventSink` push-stream helper, the injectable
8
+ :class:`ChildTransport` boundary, and the routing surface itself
9
+ (:class:`RuntimeBridge`, :class:`RuntimeBroker`, :data:`RuntimeRoute`) —
10
+ plus the behavior modules: the concrete bridges under ``bridges``, the sink
11
+ implementation, and the broker. Consumers import the routing surface from
12
+ ``induscode.runtime_bridge`` rather than reaching into individual modules.
13
+
14
+ Port note: runtime-bridge is consumed only via this barrel (the TS build's
15
+ ``src/index.ts: export * as runtimeBridge``) — it is a complete,
16
+ fully-tested library layer not yet wired into the boot path.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from induscode.runtime_bridge.bridges import (
22
+ BUILTIN_ADAPTERS,
23
+ BUILTIN_RUNTIME_SPECS,
24
+ CONTINUE,
25
+ ChildParser,
26
+ DONE,
27
+ ParseStep,
28
+ RuntimeAnnotatedModel,
29
+ annotate_card,
30
+ as_record,
31
+ builtin_runtime_spec,
32
+ claude_cli_bridge,
33
+ codex_cli_bridge,
34
+ drive_exchange,
35
+ error_text,
36
+ indusagi_cli_bridge,
37
+ make_indusagi_cli_bridge,
38
+ seed_from_model,
39
+ spec_from_model,
40
+ str_field,
41
+ with_runtime_endpoint,
42
+ )
43
+ from induscode.runtime_bridge.broker import (
44
+ RuntimeBrokerDeps,
45
+ RuntimeBrokerRuntime,
46
+ create_runtime_broker,
47
+ runtime_source_key,
48
+ )
49
+ from induscode.runtime_bridge.contract import (
50
+ NORMALIZED_EVENT_KINDS,
51
+ RUNTIME_ENDPOINT_SCHEME,
52
+ RUNTIME_LINK_ENTRY,
53
+ Api,
54
+ AssistantMessage,
55
+ AssistantMessageEventStream,
56
+ BridgeEventSink,
57
+ BridgeFailure,
58
+ ChildMessage,
59
+ ChildRequest,
60
+ ChildTransport,
61
+ ChildTransportFactory,
62
+ Context,
63
+ ExchangeOptions,
64
+ ExternalRoute,
65
+ ExternalRuntimeSpec,
66
+ FailedEvent,
67
+ FinishEvent,
68
+ FinishReason,
69
+ FrameworkRoute,
70
+ FrameworkStream,
71
+ KnownProvider,
72
+ Model,
73
+ NormalizedEvent,
74
+ NormalizedEventKind,
75
+ ResumeEvent,
76
+ RuntimeAdapterId,
77
+ RuntimeAuthMode,
78
+ RuntimeBridge,
79
+ RuntimeBroker,
80
+ RuntimeEndpointScheme,
81
+ RuntimeLink,
82
+ RuntimeLinkEntryTag,
83
+ RuntimeLinkStore,
84
+ RuntimeRoute,
85
+ SimpleStreamOptions,
86
+ StopReason,
87
+ TextEvent,
88
+ ThinkingEvent,
89
+ ToolCall,
90
+ ToolCallEvent,
91
+ TransportContext,
92
+ runtime_endpoint,
93
+ )
94
+ from induscode.runtime_bridge.sink import BridgeMessageSeed, create_bridge_sink
95
+
96
+ __all__ = [
97
+ "Api",
98
+ "AssistantMessage",
99
+ "AssistantMessageEventStream",
100
+ "BUILTIN_ADAPTERS",
101
+ "BUILTIN_RUNTIME_SPECS",
102
+ "BridgeEventSink",
103
+ "BridgeFailure",
104
+ "BridgeMessageSeed",
105
+ "CONTINUE",
106
+ "ChildMessage",
107
+ "ChildParser",
108
+ "ChildRequest",
109
+ "ChildTransport",
110
+ "ChildTransportFactory",
111
+ "Context",
112
+ "DONE",
113
+ "ExchangeOptions",
114
+ "ExternalRoute",
115
+ "ExternalRuntimeSpec",
116
+ "FailedEvent",
117
+ "FinishEvent",
118
+ "FinishReason",
119
+ "FrameworkRoute",
120
+ "FrameworkStream",
121
+ "KnownProvider",
122
+ "Model",
123
+ "NORMALIZED_EVENT_KINDS",
124
+ "NormalizedEvent",
125
+ "NormalizedEventKind",
126
+ "ParseStep",
127
+ "RUNTIME_ENDPOINT_SCHEME",
128
+ "RUNTIME_LINK_ENTRY",
129
+ "ResumeEvent",
130
+ "RuntimeAdapterId",
131
+ "RuntimeAnnotatedModel",
132
+ "RuntimeAuthMode",
133
+ "RuntimeBridge",
134
+ "RuntimeBroker",
135
+ "RuntimeBrokerDeps",
136
+ "RuntimeBrokerRuntime",
137
+ "RuntimeEndpointScheme",
138
+ "RuntimeLink",
139
+ "RuntimeLinkEntryTag",
140
+ "RuntimeLinkStore",
141
+ "RuntimeRoute",
142
+ "SimpleStreamOptions",
143
+ "StopReason",
144
+ "TextEvent",
145
+ "ThinkingEvent",
146
+ "ToolCall",
147
+ "ToolCallEvent",
148
+ "TransportContext",
149
+ "annotate_card",
150
+ "as_record",
151
+ "builtin_runtime_spec",
152
+ "claude_cli_bridge",
153
+ "codex_cli_bridge",
154
+ "create_bridge_sink",
155
+ "create_runtime_broker",
156
+ "drive_exchange",
157
+ "error_text",
158
+ "indusagi_cli_bridge",
159
+ "make_indusagi_cli_bridge",
160
+ "runtime_endpoint",
161
+ "runtime_source_key",
162
+ "seed_from_model",
163
+ "spec_from_model",
164
+ "str_field",
165
+ "with_runtime_endpoint",
166
+ ]
@@ -0,0 +1,66 @@
1
+ """Bridges subsystem — public barrel (port of TS
2
+ ``src/runtime-bridge/bridges/index.ts``).
3
+
4
+ Bundles the three shipped external-runtime
5
+ :class:`~induscode.runtime_bridge.contract.RuntimeBridge`\\ s — the
6
+ Anthropic-flavoured ``claude-cli``, the OpenAI-flavoured ``codex-cli``, and
7
+ the peer JSON-RPC ``indusagi-cli`` — plus the built-in runtime spec catalog
8
+ and the model-annotation helpers. Consumers register the bridges with a
9
+ :class:`~induscode.runtime_bridge.contract.RuntimeBroker` and annotate
10
+ catalog cards via this barrel rather than reaching into the individual
11
+ modules.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # --- Shared driver surface (for bridge authors / tests) ---------------------
17
+ from ._drive import (
18
+ CONTINUE,
19
+ DONE,
20
+ ChildParser,
21
+ ParseStep,
22
+ as_record,
23
+ drive_exchange,
24
+ error_text,
25
+ seed_from_model,
26
+ str_field,
27
+ )
28
+
29
+ # --- Built-in runtime catalog + annotation ----------------------------------
30
+ from .builtins import (
31
+ BUILTIN_ADAPTERS,
32
+ BUILTIN_RUNTIME_SPECS,
33
+ RuntimeAnnotatedModel,
34
+ annotate_card,
35
+ builtin_runtime_spec,
36
+ spec_from_model,
37
+ with_runtime_endpoint,
38
+ )
39
+
40
+ # --- Bridges ----------------------------------------------------------------
41
+ from .claude_cli import claude_cli_bridge
42
+ from .codex_cli import codex_cli_bridge
43
+ from .indusagi_cli import indusagi_cli_bridge, make_indusagi_cli_bridge
44
+
45
+ __all__ = [
46
+ "BUILTIN_ADAPTERS",
47
+ "BUILTIN_RUNTIME_SPECS",
48
+ "CONTINUE",
49
+ "ChildParser",
50
+ "DONE",
51
+ "ParseStep",
52
+ "RuntimeAnnotatedModel",
53
+ "annotate_card",
54
+ "as_record",
55
+ "builtin_runtime_spec",
56
+ "claude_cli_bridge",
57
+ "codex_cli_bridge",
58
+ "drive_exchange",
59
+ "error_text",
60
+ "indusagi_cli_bridge",
61
+ "make_indusagi_cli_bridge",
62
+ "seed_from_model",
63
+ "spec_from_model",
64
+ "str_field",
65
+ "with_runtime_endpoint",
66
+ ]
@@ -0,0 +1,268 @@
1
+ """Shared exchange driver — the transport-wiring half every bridge reuses
2
+ (port of TS ``src/runtime-bridge/bridges/_drive.ts``).
3
+
4
+ A bridge is meant to be *only* a per-dialect parser. Two concerns are common
5
+ to all three shipped bridges and are factored here so a bridge never repeats
6
+ them:
7
+
8
+ 1. The lifecycle wiring: subscribe to the
9
+ :class:`~induscode.runtime_bridge.contract.ChildTransport`, forward each
10
+ inbound :class:`~induscode.runtime_bridge.contract.ChildMessage` to the
11
+ bridge's parser, settle the sink on a terminal parser result, honour the
12
+ caller's :class:`~indusagi._internal.cancel.CancelToken` (the port-wide
13
+ ``AbortSignal`` replacement), and always dispose the subscription +
14
+ transport on the way out.
15
+ 2. The seed: deriving the
16
+ :class:`~induscode.runtime_bridge.sink.BridgeMessageSeed` the sink stamps
17
+ onto the accumulated message from the bound model.
18
+
19
+ A bridge supplies a :data:`ChildParser` (pure: child payload -> parser
20
+ result) and an opening :class:`~induscode.runtime_bridge.contract.ChildRequest`;
21
+ :func:`drive_exchange` does the rest and returns the populated
22
+ ``AssistantMessageEventStream``.
23
+
24
+ Crucially this module touches the transport only through the injected
25
+ interface — no subprocess module, no spawn. A test hands a fake transport
26
+ and drives the parser end-to-end with zero real binaries.
27
+
28
+ Async-translation notes (TS -> Python):
29
+
30
+ - TS ``queueMicrotask`` (the pre-aborted settle, deliberately off-stack so
31
+ the caller receives the stream before the error lands) becomes
32
+ ``loop.call_soon``.
33
+ - TS ``void transport.send(request).catch(...)`` / ``void transport.close()``
34
+ become fire-and-forget asyncio tasks tracked in a module-level set (never
35
+ bare, so they are not garbage-collected mid-flight) with exceptions routed
36
+ to the settle path / swallowed respectively.
37
+ - The driver therefore requires a **running event loop** at call time — the
38
+ Python analogue of the always-present JS microtask queue.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import asyncio
44
+ from collections.abc import Callable, Mapping
45
+ from dataclasses import dataclass
46
+ from typing import Any, Final, Literal, TypeAlias
47
+
48
+ from induscode.runtime_bridge.contract import (
49
+ AssistantMessageEventStream,
50
+ BridgeEventSink,
51
+ BridgeFailure,
52
+ ChildMessage,
53
+ ChildRequest,
54
+ ChildTransport,
55
+ ExchangeOptions,
56
+ Model,
57
+ )
58
+ from induscode.runtime_bridge.sink import BridgeMessageSeed, create_bridge_sink
59
+
60
+ __all__ = [
61
+ "CONTINUE",
62
+ "ChildParser",
63
+ "DONE",
64
+ "ParseStep",
65
+ "as_record",
66
+ "drive_exchange",
67
+ "error_text",
68
+ "seed_from_model",
69
+ "str_field",
70
+ ]
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Parser surface
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class ParseStep:
80
+ """The outcome a parser reports for one inbound child payload (TS
81
+ ``ParseStep``). A parser maps a single child payload to the normalized
82
+ events it produced and whether that payload terminated the exchange.
83
+
84
+ - ``"continue"`` — zero or more events were emitted; the stream stays
85
+ open.
86
+ - ``"done"`` — the exchange settled (the parser already emitted the
87
+ ``finish`` / ``failed`` event, or the driver should finish with the
88
+ default reason). No further payloads are processed.
89
+ """
90
+
91
+ status: Literal["continue", "done"]
92
+
93
+
94
+ #: A ``continue`` parse step (no terminal payload).
95
+ CONTINUE: Final = ParseStep(status="continue")
96
+ #: A ``done`` parse step (the payload ended the exchange).
97
+ DONE: Final = ParseStep(status="done")
98
+
99
+ #: A per-dialect parser: interpret one inbound child payload, drive the sink
100
+ #: with whatever normalized events it yields, and report whether the exchange
101
+ #: is over (TS ``ChildParser``). The parser owns *interpretation* only; it
102
+ #: never touches the framework stream directly — it calls sink methods
103
+ #: (``text`` / ``thinking`` / ``tool_call`` / ``emit`` / ``finish_success`` /
104
+ #: ``finish_error``). Returning :data:`DONE` tells the driver to stop and (if
105
+ #: the sink is not already settled) finish successfully.
106
+ ChildParser: TypeAlias = Callable[[ChildMessage, BridgeEventSink], ParseStep]
107
+
108
+
109
+ def seed_from_model(model: Model) -> BridgeMessageSeed:
110
+ """Derive the message seed the sink stamps onto its accumulated message."""
111
+ return BridgeMessageSeed(
112
+ api=str(_model_field(model, "api")),
113
+ provider=str(_model_field(model, "provider")),
114
+ model=str(_model_field(model, "id")),
115
+ )
116
+
117
+
118
+ def _model_field(model: Any, key: str) -> Any:
119
+ """Tolerant model-field read: framework ``Model`` dataclasses (the live
120
+ path) and plain mappings (test/catalog sources) both work, mirroring JS
121
+ property access."""
122
+ if isinstance(model, Mapping):
123
+ return model.get(key)
124
+ return getattr(model, key, None)
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Fire-and-forget task bookkeeping
129
+ # ---------------------------------------------------------------------------
130
+
131
+ #: Live fire-and-forget tasks (send / close). Held so the event loop cannot
132
+ #: garbage-collect them mid-flight; a done-callback prunes each on settle.
133
+ _PENDING_TASKS: set[asyncio.Task[None]] = set()
134
+
135
+
136
+ def _spawn(loop: asyncio.AbstractEventLoop, coro: Any) -> None:
137
+ task = loop.create_task(coro)
138
+ _PENDING_TASKS.add(task)
139
+ task.add_done_callback(_PENDING_TASKS.discard)
140
+
141
+
142
+ async def _close_quietly(transport: ChildTransport) -> None:
143
+ try:
144
+ await transport.close()
145
+ except Exception:
146
+ # best-effort: a close failure must not mask the streamed outcome.
147
+ pass
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # The driver
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ def drive_exchange(
156
+ model: Model,
157
+ opts: ExchangeOptions,
158
+ transport: ChildTransport,
159
+ request: ChildRequest,
160
+ parse: ChildParser,
161
+ ) -> AssistantMessageEventStream:
162
+ """Drive a full exchange over an injected transport (TS
163
+ ``driveExchange``).
164
+
165
+ Returns the sink's ``AssistantMessageEventStream`` synchronously; the
166
+ stream is populated asynchronously as the child emits. The driver:
167
+
168
+ - subscribes the parser to inbound messages;
169
+ - sends the opening request;
170
+ - settles + tears down on a terminal parse step, transport closure,
171
+ cancellation, or thrown parser error.
172
+
173
+ :param model: the bound model (seeds the sink identity)
174
+ :param opts: per-exchange options (only ``signal`` is consulted here)
175
+ :param transport: the injected child boundary
176
+ :param request: the opening request to send once subscribed
177
+ :param parse: the per-dialect parser
178
+ """
179
+ sink = create_bridge_sink(seed_from_model(model))
180
+ loop = asyncio.get_running_loop()
181
+
182
+ finished = False
183
+ unsubscribe: Callable[[], None] | None = None
184
+
185
+ def teardown() -> None:
186
+ nonlocal unsubscribe
187
+ if unsubscribe is not None:
188
+ unsubscribe()
189
+ unsubscribe = None
190
+ _spawn(loop, _close_quietly(transport))
191
+
192
+ def settle_success() -> None:
193
+ nonlocal finished
194
+ if finished:
195
+ return
196
+ finished = True
197
+ sink.finish_success()
198
+ teardown()
199
+
200
+ def settle_error(message: str, aborted: bool, cause: Any = None) -> None:
201
+ nonlocal finished
202
+ if finished:
203
+ return
204
+ finished = True
205
+ sink.finish_error(BridgeFailure(message=message, aborted=aborted, cause=cause))
206
+ teardown()
207
+
208
+ signal = getattr(opts, "signal", None)
209
+ if signal is not None:
210
+ if signal.cancelled:
211
+ # Already cancelled before we started: surface an aborted failure
212
+ # off-stack (TS queueMicrotask -> loop.call_soon, so the caller
213
+ # holds the stream before the error lands) and make no transport
214
+ # calls beyond teardown.
215
+ loop.call_soon(lambda: settle_error("exchange aborted", True))
216
+ return sink.stream
217
+ # CancelToken.add_callback mirrors addEventListener("abort", …,
218
+ # {once: true}); settle_error is idempotent via the finished flag.
219
+ signal.add_callback(lambda: settle_error("exchange aborted", True))
220
+
221
+ def on_message(message: ChildMessage) -> None:
222
+ if finished:
223
+ return
224
+ try:
225
+ step = parse(message, sink)
226
+ if step.status == "done":
227
+ settle_success()
228
+ except Exception as cause:
229
+ settle_error(error_text(cause), False, cause)
230
+
231
+ unsubscribe = transport.on_message(on_message)
232
+
233
+ # Send the opening request; a send failure ends the exchange in error.
234
+ async def _send() -> None:
235
+ try:
236
+ await transport.send(request)
237
+ except Exception as cause:
238
+ settle_error(error_text(cause), False, cause)
239
+
240
+ _spawn(loop, _send())
241
+
242
+ return sink.stream
243
+
244
+
245
+ def error_text(cause: Any) -> str:
246
+ """Best-effort single-line message for an unknown thrown value (TS
247
+ ``errorText``)."""
248
+ if isinstance(cause, BaseException):
249
+ return str(cause)
250
+ if isinstance(cause, str):
251
+ return cause
252
+ return "external runtime error"
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # Payload-shape helpers (shared by the dialect parsers)
257
+ # ---------------------------------------------------------------------------
258
+
259
+
260
+ def as_record(value: Any) -> Mapping[str, Any] | None:
261
+ """Narrow an opaque payload to a string-keyed record (or ``None``)."""
262
+ return value if isinstance(value, Mapping) else None
263
+
264
+
265
+ def str_field(record: Mapping[str, Any], key: str) -> str | None:
266
+ """Read a string field off a record, or ``None`` when absent/non-string."""
267
+ value = record.get(key)
268
+ return value if isinstance(value, str) else None
@@ -0,0 +1,177 @@
1
+ """Built-in external-runtime catalog — the shipped
2
+ :class:`~induscode.runtime_bridge.contract.ExternalRuntimeSpec` entries and
3
+ the helper that *annotates* a model with one (port of TS
4
+ ``src/runtime-bridge/bridges/builtins.ts``).
5
+
6
+ The model catalog (:mod:`induscode.conductor.catalog`) owns the model list;
7
+ this layer never re-catalogues. Instead it carries a small table of the
8
+ three runtimes this build ships and a single sanctioned way to stamp a
9
+ runtime onto a model: rewrite the model's ``baseUrl`` to the synthetic
10
+ ``bridge:<adapter>`` endpoint (so a card has a stable, non-HTTP address) and
11
+ return the runtime annotation alongside it.
12
+
13
+ Why a ``baseUrl`` rewrite rather than a new field on the card: the catalog
14
+ card shape is frozen and the framework ``Model`` has a fixed shape; the
15
+ ``bridge:<adapter>`` convention lets a routing consumer recognize a
16
+ runtime-backed model purely from its address
17
+ (``baseUrl.startswith(scheme)``), with the full spec resolved from this
18
+ table — no schema change to the catalog.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import dataclasses
24
+ from collections.abc import Mapping
25
+ from dataclasses import dataclass
26
+ from typing import Any, Final
27
+
28
+ # The single cross-subsystem import (runtime-bridge → conductor), exactly as
29
+ # in the TS source.
30
+ from induscode.conductor.catalog import CatalogCard
31
+ from induscode.runtime_bridge.contract import (
32
+ RUNTIME_ENDPOINT_SCHEME,
33
+ ExternalRuntimeSpec,
34
+ Model,
35
+ RuntimeAdapterId,
36
+ runtime_endpoint,
37
+ )
38
+
39
+ __all__ = [
40
+ "BUILTIN_ADAPTERS",
41
+ "BUILTIN_RUNTIME_SPECS",
42
+ "RuntimeAnnotatedModel",
43
+ "annotate_card",
44
+ "builtin_runtime_spec",
45
+ "spec_from_model",
46
+ "with_runtime_endpoint",
47
+ ]
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Shipped runtime specs
52
+ # ---------------------------------------------------------------------------
53
+
54
+ #: The three runtimes this build ships, keyed by adapter id.
55
+ #:
56
+ #: Each is ``external-cli`` (the spawned child owns its own login), so a
57
+ #: model bound to one is offered as available with an empty credential
58
+ #: vault. The ``binaryPath`` names the default executable the broker
59
+ #: launches when a model's spec does not override it; ``args`` are extra
60
+ #: flags prepended ahead of the bridge's own protocol flags.
61
+ BUILTIN_RUNTIME_SPECS: Final[Mapping[RuntimeAdapterId, ExternalRuntimeSpec]] = {
62
+ "claude-cli": ExternalRuntimeSpec(
63
+ adapter="claude-cli",
64
+ authMode="external-cli",
65
+ binaryPath="claude",
66
+ args=("--output-format", "stream-json", "--verbose"),
67
+ ),
68
+ "codex-cli": ExternalRuntimeSpec(
69
+ adapter="codex-cli",
70
+ authMode="external-cli",
71
+ binaryPath="codex",
72
+ args=("--json",),
73
+ ),
74
+ "indusagi-cli": ExternalRuntimeSpec(
75
+ adapter="indusagi-cli",
76
+ authMode="external-cli",
77
+ binaryPath="indusagi",
78
+ args=("--rpc",),
79
+ ),
80
+ }
81
+
82
+ #: The shipped adapter ids, in catalog order.
83
+ BUILTIN_ADAPTERS: Final[tuple[RuntimeAdapterId, ...]] = (
84
+ "claude-cli",
85
+ "codex-cli",
86
+ "indusagi-cli",
87
+ )
88
+
89
+
90
+ def builtin_runtime_spec(adapter: RuntimeAdapterId) -> ExternalRuntimeSpec | None:
91
+ """Look up a shipped :class:`ExternalRuntimeSpec` by adapter id
92
+ (``None`` if none)."""
93
+ return BUILTIN_RUNTIME_SPECS.get(adapter)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Annotation
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class RuntimeAnnotatedModel:
103
+ """A model annotated with its external runtime (TS
104
+ ``RuntimeAnnotatedModel``): the rewritten framework model (its
105
+ ``baseUrl`` now the ``bridge:<adapter>`` synthetic endpoint) paired with
106
+ the resolved :class:`ExternalRuntimeSpec`. This is the value a routing
107
+ consumer stores in its side table keyed by canonical id."""
108
+
109
+ #: The framework model with its ``baseUrl`` rewritten to the synthetic endpoint.
110
+ model: Model
111
+ #: The runtime spec the broker drives this model through.
112
+ spec: ExternalRuntimeSpec
113
+
114
+
115
+ def _model_field(model: Any, key: str) -> Any:
116
+ """Tolerant model-field read (dataclass attribute or mapping key) —
117
+ catalog cards retain raw records verbatim, which test sources may supply
118
+ as plain mappings."""
119
+ if isinstance(model, Mapping):
120
+ return model.get(key)
121
+ return getattr(model, key, None)
122
+
123
+
124
+ def with_runtime_endpoint(model: Model, spec: ExternalRuntimeSpec) -> Model:
125
+ """Rewrite a framework ``Model``'s ``baseUrl`` to the synthetic
126
+ ``bridge:<adapter>`` endpoint for ``spec.adapter``. Returns a fresh
127
+ model; the input is never mutated (TS object spread →
128
+ :func:`dataclasses.replace`)."""
129
+ endpoint = runtime_endpoint(spec.adapter)
130
+ if dataclasses.is_dataclass(model) and not isinstance(model, type):
131
+ return dataclasses.replace(model, baseUrl=endpoint)
132
+ # Mapping-shaped raw records (test/catalog sources): the TS spread idiom.
133
+ return {**model, "baseUrl": endpoint} # type: ignore[return-value]
134
+
135
+
136
+ def annotate_card(
137
+ card: CatalogCard,
138
+ adapter: RuntimeAdapterId,
139
+ spec: ExternalRuntimeSpec | None = None,
140
+ ) -> RuntimeAnnotatedModel | None:
141
+ """Annotate one :class:`~induscode.conductor.catalog.CatalogCard` with a
142
+ runtime spec.
143
+
144
+ Resolves the spec from ``spec`` when given explicitly, else from the
145
+ shipped table by ``adapter``. Returns the card's framework model
146
+ rewritten to the synthetic endpoint plus the resolved spec, or ``None``
147
+ when no spec is known for ``adapter`` (so a caller can skip non-runtime
148
+ adapters).
149
+
150
+ :param card: the catalog card whose model to annotate
151
+ :param adapter: the runtime adapter to bind the model to
152
+ :param spec: an explicit spec override; defaults to the shipped one
153
+ """
154
+ resolved = spec if spec is not None else builtin_runtime_spec(adapter)
155
+ if resolved is None:
156
+ return None
157
+ return RuntimeAnnotatedModel(model=with_runtime_endpoint(card.model, resolved), spec=resolved)
158
+
159
+
160
+ def spec_from_model(model: Model) -> ExternalRuntimeSpec | None:
161
+ """Decode the runtime annotation off a framework ``Model``, the inverse
162
+ of :func:`with_runtime_endpoint`. Returns the shipped
163
+ :class:`ExternalRuntimeSpec` for the adapter named in a
164
+ ``bridge:<adapter>`` ``baseUrl``, or ``None`` when the model is a plain
165
+ HTTP-provider model (its ``baseUrl`` is a real URL) or the named adapter
166
+ is not a shipped runtime.
167
+
168
+ This is the lookup a broker uses to recognize a runtime-backed model
169
+ purely from its address.
170
+
171
+ :param model: the model whose ``baseUrl`` to inspect
172
+ """
173
+ base_url = _model_field(model, "baseUrl")
174
+ if not isinstance(base_url, str) or not base_url.startswith(RUNTIME_ENDPOINT_SCHEME):
175
+ return None
176
+ adapter = base_url[len(RUNTIME_ENDPOINT_SCHEME) :]
177
+ return builtin_runtime_spec(adapter) if len(adapter) > 0 else None