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,351 @@
1
+ """The single :class:`~induscode.runtime_bridge.contract.BridgeEventSink`
2
+ implementation (port of TS ``src/runtime-bridge/sink.ts``) — the one place
3
+ the imperative push-stream idiom every bridge shares is written down.
4
+
5
+ Each external runtime emits its own wire dialect; a bridge's parser maps
6
+ that dialect onto the provider-neutral
7
+ :data:`~induscode.runtime_bridge.contract.NormalizedEvent` union and drives a
8
+ sink. This module owns everything else:
9
+
10
+ - the accumulating ``AssistantMessage`` every framework event carries as its
11
+ ``partial``;
12
+ - the content-block index bookkeeping (a block's index is its position in
13
+ the content sequence, opened lazily on first emission of its kind);
14
+ - the lazily-started lifecycle (``start`` is implicit on first emission, and
15
+ idempotent) and the terminal ``finish_success`` / ``finish_error`` settle;
16
+ - the ``NormalizedEvent -> framework event`` mapping, in :meth:`BridgeSink.emit`,
17
+ so the per-bridge code stays a pure parser.
18
+
19
+ Frozen-message adaptation (the port's one structural change — PLAN M2 /
20
+ analysis 01 risk-1)
21
+ --------------------------------------------------------------------------
22
+ The TS sink mutates one shared ``partial`` ``AssistantMessage`` in place
23
+ (``block.text += delta``) and hands the *same object* to every push call.
24
+ The Python framework's ``AssistantMessage`` is a **frozen** dataclass whose
25
+ ``content`` is a **tuple** of frozen parts — in-place mutation is
26
+ impossible. So this port keeps a **mutable builder** (a plain list of
27
+ ``["text", buffer]`` / ``["thinking", buffer]`` / ``["toolCall", ToolCall]``
28
+ parts whose string buffers accumulate across deltas) and **materializes a
29
+ fresh frozen ``AssistantMessage`` snapshot per push** — the exact idiom the
30
+ framework's own facade stream uses (``indusagi.ai.stream._Accumulator``:
31
+ "Snapshots materialize a fresh frozen ``AssistantMessage`` per event …
32
+ consumers only ever read it at delivery time, which snapshots reproduce
33
+ exactly"). A block's index is its position in the builder list, so the
34
+ content-index bookkeeping is unchanged from TS.
35
+
36
+ The framework push surface this writes through — ``push_start /
37
+ push_text_start / push_text_delta / push_text_end / push_thinking_start /
38
+ push_thinking_delta / push_thinking_end / push_tool_call_start /
39
+ push_tool_call_delta / push_tool_call_end / push_done / push_error`` on
40
+ ``AssistantMessageEventStream`` (verified against
41
+ ``indusagi/ai/events.py``) — is consumed verbatim from the framework via
42
+ :func:`indusagi.ai.create_assistant_message_event_stream`; this module never
43
+ re-derives event shapes.
44
+
45
+ Streaming contract honored (matching the framework's own adapters): a text
46
+ or thinking block opens with a ``*_start`` once the block has been appended
47
+ to the builder, streams ``*_delta``\\ s while the running content
48
+ accumulates, and closes with a ``*_end`` carrying the final content. A tool
49
+ call is a single self-contained block (``start -> delta(args) -> end``). The
50
+ terminal ``done`` / ``error`` event carries the fully-accumulated message.
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import json
56
+ import time
57
+ from collections.abc import Callable
58
+ from dataclasses import dataclass
59
+ from typing import Any, Final, Literal
60
+
61
+ # The push stream is constructed through the framework's factory value; the
62
+ # message/content types come from the same barrel.
63
+ from indusagi.ai import (
64
+ AssistantMessage,
65
+ AssistantMessageEventStream,
66
+ TextContent,
67
+ ThinkingContent,
68
+ ToolCall,
69
+ create_assistant_message_event_stream,
70
+ create_zero_usage,
71
+ )
72
+
73
+ from induscode.runtime_bridge.contract import (
74
+ NORMALIZED_EVENT_KINDS,
75
+ BridgeEventSink,
76
+ BridgeFailure,
77
+ FinishReason,
78
+ NormalizedEvent,
79
+ NormalizedEventKind,
80
+ )
81
+
82
+ __all__ = [
83
+ "BridgeMessageSeed",
84
+ "create_bridge_sink",
85
+ ]
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Message identity seed
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class BridgeMessageSeed:
95
+ """The model-identity fields stamped onto every ``partial``
96
+ ``AssistantMessage`` the sink emits (TS ``BridgeMessageSeed``). A bridge
97
+ supplies these from the bound model so a runtime-backed turn reports the
98
+ same ``api`` / ``provider`` / ``model`` triple a network turn would —
99
+ keeping the accumulated message uniform regardless of which path
100
+ produced it."""
101
+
102
+ #: The bound model's ``api`` dialect identifier.
103
+ api: str
104
+ #: The bound model's provider slug.
105
+ provider: str
106
+ #: The bound model's id (the value stamped into ``AssistantMessage.model``).
107
+ model: str
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Internal: open-block tracking
112
+ # ---------------------------------------------------------------------------
113
+
114
+ # The currently-open streamable content block: ("text" | "thinking", index).
115
+ # Text and thinking blocks stay open across their deltas so a later delta of
116
+ # a *different* kind first closes the prior block (emitting its `*_end`)
117
+ # before opening its own — mirroring the framework's one-block-at-a-time
118
+ # content stream. A tool call never lingers as the open block (it opens and
119
+ # closes atomically), so it is not tracked here.
120
+ _OpenBlock = tuple[Literal["text", "thinking"], int]
121
+
122
+
123
+ def _now_ms() -> int:
124
+ """Milliseconds since the Unix epoch (TS ``Date.now()``)."""
125
+ return int(time.time() * 1000)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Sink implementation
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ class BridgeSink:
134
+ """The concrete :class:`BridgeEventSink`. Private to the module; bridges
135
+ construct one through :func:`create_bridge_sink`."""
136
+
137
+ __slots__ = (
138
+ "_error_message",
139
+ "_open",
140
+ "_parts",
141
+ "_seed",
142
+ "_settled",
143
+ "_started",
144
+ "_stop_reason",
145
+ "_stream",
146
+ "_timestamp",
147
+ )
148
+
149
+ def __init__(self, seed: BridgeMessageSeed) -> None:
150
+ self._stream: AssistantMessageEventStream = create_assistant_message_event_stream()
151
+ self._seed = seed
152
+ # The MUTABLE builder behind the frozen snapshots. Each part:
153
+ # ["text", buffer] | ["thinking", buffer] | ["toolCall", ToolCall]
154
+ # A block's index is its position in this list (== its contentIndex).
155
+ self._parts: list[list[Any]] = []
156
+ #: The currently-open streamable block, if any.
157
+ self._open: _OpenBlock | None = None
158
+ #: Whether :meth:`start` has pushed the framework ``start`` event.
159
+ self._started = False
160
+ #: Whether a terminal ``done`` / ``error`` has been pushed.
161
+ self._settled = False
162
+ # Snapshot fields the TS sink mutated on the shared partial.
163
+ self._stop_reason: str = "stop"
164
+ self._error_message: str | None = None
165
+ # One message identity per exchange, like the TS partial's timestamp.
166
+ self._timestamp = _now_ms()
167
+
168
+ # ---- snapshot construction ----
169
+
170
+ def _snapshot(self) -> AssistantMessage:
171
+ """Materialize a fresh frozen ``AssistantMessage`` from the mutable
172
+ builder — the per-push ``partial``. External runtimes do not report
173
+ framework token accounting on the streamed event surface (a CLI owns
174
+ its own metering), so the message carries a zeroed usage rather than
175
+ fabricated numbers."""
176
+ content: list[TextContent | ThinkingContent | ToolCall] = []
177
+ for part in self._parts:
178
+ if part[0] == "text":
179
+ content.append(TextContent(text=part[1]))
180
+ elif part[0] == "thinking":
181
+ content.append(ThinkingContent(thinking=part[1]))
182
+ else: # ["toolCall", ToolCall] — already frozen, reused as-is.
183
+ content.append(part[1])
184
+ return AssistantMessage(
185
+ content=tuple(content),
186
+ api=self._seed.api,
187
+ provider=self._seed.provider,
188
+ model=self._seed.model,
189
+ usage=create_zero_usage(),
190
+ stopReason=self._stop_reason, # type: ignore[arg-type]
191
+ errorMessage=self._error_message,
192
+ timestamp=self._timestamp,
193
+ )
194
+
195
+ # ---- lifecycle ----
196
+
197
+ @property
198
+ def stream(self) -> AssistantMessageEventStream:
199
+ """The framework push stream handed back to the broker's caller."""
200
+ return self._stream
201
+
202
+ def start(self) -> None:
203
+ if self._started or self._settled:
204
+ return
205
+ self._started = True
206
+ self._stream.push_start(self._snapshot())
207
+
208
+ # ---- text ----
209
+
210
+ def text(self, delta: str) -> None:
211
+ if self._settled:
212
+ return
213
+ self.start()
214
+ index = self._ensure_open("text")
215
+ self._parts[index][1] += delta
216
+ self._stream.push_text_delta(index, delta, self._snapshot())
217
+
218
+ # ---- thinking ----
219
+
220
+ def thinking(self, delta: str) -> None:
221
+ if self._settled:
222
+ return
223
+ self.start()
224
+ index = self._ensure_open("thinking")
225
+ self._parts[index][1] += delta
226
+ self._stream.push_thinking_delta(index, delta, self._snapshot())
227
+
228
+ # ---- tool call ----
229
+
230
+ def tool_call(self, call: ToolCall) -> None:
231
+ if self._settled:
232
+ return
233
+ self.start()
234
+ # A tool call is a self-contained block: close any open text/thinking
235
+ # block first so content ordering stays linear, then open/stream/close
236
+ # atomically.
237
+ self._close_open()
238
+ index = len(self._parts)
239
+ block = ToolCall(
240
+ id=call.id,
241
+ name=call.name,
242
+ arguments=call.arguments,
243
+ thoughtSignature=call.thoughtSignature,
244
+ )
245
+ self._parts.append(["toolCall", block])
246
+ self._stream.push_tool_call_start(index, self._snapshot())
247
+ self._stream.push_tool_call_delta(
248
+ index,
249
+ json.dumps(dict(block.arguments), separators=(",", ":")),
250
+ self._snapshot(),
251
+ )
252
+ self._stream.push_tool_call_end(index, block, self._snapshot())
253
+
254
+ # ---- normalized-event dispatch ----
255
+
256
+ def emit(self, event: NormalizedEvent) -> None:
257
+ _EMIT_DISPATCH[event.kind](self, event)
258
+
259
+ # ---- terminal: success ----
260
+
261
+ def finish_success(self, reason: FinishReason = "stop") -> None:
262
+ if self._settled:
263
+ return
264
+ self.start()
265
+ self._close_open()
266
+ self._settled = True
267
+ self._stop_reason = reason
268
+ self._stream.push_done(reason, self._snapshot())
269
+
270
+ # ---- terminal: error ----
271
+
272
+ def finish_error(self, error: BridgeFailure) -> None:
273
+ if self._settled:
274
+ return
275
+ self.start()
276
+ self._close_open()
277
+ self._settled = True
278
+ reason: Literal["aborted", "error"] = "aborted" if error.aborted else "error"
279
+ self._stop_reason = reason
280
+ self._error_message = error.message
281
+ self._stream.push_error(reason, self._snapshot())
282
+
283
+ # ------------------------------------------------------------------
284
+ # Internal block management
285
+ # ------------------------------------------------------------------
286
+
287
+ def _ensure_open(self, kind: Literal["text", "thinking"]) -> int:
288
+ """Ensure the open block is of ``kind``, returning its content index.
289
+ If a block of a different kind is open it is closed first; if none is
290
+ open (or the kind differs), a fresh block is appended and its
291
+ ``*_start`` is pushed."""
292
+ if self._open is not None:
293
+ if self._open[0] == kind:
294
+ return self._open[1]
295
+ self._close_open()
296
+ index = len(self._parts)
297
+ if kind == "text":
298
+ self._parts.append(["text", ""])
299
+ self._stream.push_text_start(index, self._snapshot())
300
+ else:
301
+ self._parts.append(["thinking", ""])
302
+ self._stream.push_thinking_start(index, self._snapshot())
303
+ self._open = (kind, index)
304
+ return index
305
+
306
+ def _close_open(self) -> None:
307
+ """Close the currently-open text/thinking block, pushing its
308
+ ``*_end`` with the accumulated content. No-op when nothing is open."""
309
+ open_ = self._open
310
+ if open_ is None:
311
+ return
312
+ self._open = None
313
+ kind, index = open_
314
+ if kind == "text":
315
+ self._stream.push_text_end(index, self._parts[index][1], self._snapshot())
316
+ else:
317
+ self._stream.push_thinking_end(index, self._parts[index][1], self._snapshot())
318
+
319
+
320
+ # The NormalizedEvent -> sink-method dispatch (the TS exhaustive `switch`).
321
+ # Keys are pinned against NORMALIZED_EVENT_KINDS by a key-coverage test
322
+ # (PLAN rule 1: exhaustiveness via tests, not types). A `resume` event is
323
+ # informational: the bridge persists the resume token out-of-band; the event
324
+ # stream carries no resume signal, so it is a no-op here.
325
+ _EMIT_DISPATCH: Final[dict[NormalizedEventKind, Callable[[BridgeSink, Any], None]]] = {
326
+ "text": lambda sink, event: sink.text(event.delta),
327
+ "thinking": lambda sink, event: sink.thinking(event.delta),
328
+ "tool_call": lambda sink, event: sink.tool_call(event.call),
329
+ "resume": lambda sink, event: None,
330
+ "finish": lambda sink, event: sink.finish_success(event.reason),
331
+ "failed": lambda sink, event: sink.finish_error(event.error),
332
+ }
333
+
334
+ assert set(_EMIT_DISPATCH) == set(NORMALIZED_EVENT_KINDS)
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # Factory
339
+ # ---------------------------------------------------------------------------
340
+
341
+
342
+ def create_bridge_sink(seed: BridgeMessageSeed) -> BridgeEventSink:
343
+ """Construct a :class:`BridgeEventSink` seeded with the bound model's
344
+ identity (TS ``createBridgeSink``). The single sanctioned way a bridge
345
+ obtains a sink: it creates one, drives it with normalized events (or the
346
+ convenience emitters), and returns :attr:`BridgeEventSink.stream`. The
347
+ imperative push idiom lives entirely inside the returned sink.
348
+
349
+ :param seed: the bound model's ``api`` / ``provider`` / ``model`` identity
350
+ """
351
+ return BridgeSink(seed)
@@ -0,0 +1,25 @@
1
+ """Sessions subsystem — public barrel (port of TS ``src/sessions/index.ts``).
2
+
3
+ Exposes the :class:`SessionLibrary` (the catalog-and-navigation layer over
4
+ the conductor's persisted transcripts) and its value contract — the
5
+ :class:`SavedSession` catalog row, the :class:`BranchNode` navigator row, and
6
+ the :class:`PriorTurn` fork candidate. Consumers import the session-library
7
+ surface from ``induscode.sessions`` rather than reaching into the individual
8
+ modules.
9
+
10
+ (The per-cwd ``--<cwd slug>--`` scope-dir helper is **not** here — it is
11
+ boot's, ported with ``boot/runners/session.py`` in the M4 launch/boot wave;
12
+ the library takes the already-scoped directory.)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from induscode.sessions.contract import BranchNode, PriorTurn, SavedSession
18
+ from induscode.sessions.library import SessionLibrary
19
+
20
+ __all__ = [
21
+ "BranchNode",
22
+ "PriorTurn",
23
+ "SavedSession",
24
+ "SessionLibrary",
25
+ ]
@@ -0,0 +1,119 @@
1
+ """Session Library contract — the value types the library surface speaks in
2
+ (port of TS ``src/sessions/contract.ts``).
3
+
4
+ The library sits on top of the conductor's persistent transcript store: it
5
+ enumerates the session files a workspace has accumulated, opens one back into
6
+ a live :class:`~induscode.conductor.transcript_store.TranscriptStore`, and
7
+ projects the resulting node tree into the flat shapes a navigator and a
8
+ forking picker consume.
9
+
10
+ These records are the *catalog/navigation* projections — the minimum a
11
+ chooser UI needs. They deliberately hold no framework message objects: text
12
+ is already reduced to a short preview, and identity is carried by stable
13
+ string ids so a caller can list, rename, delete, walk, and fork without
14
+ rehydrating payloads.
15
+
16
+ Field names keep the TS camelCase spelling (``lastModified``,
17
+ ``messageCount``, ``entryId``, ``isLeaf``, ``isCurrent``) per the port
18
+ convention.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+
25
+ __all__ = [
26
+ "BranchNode",
27
+ "PriorTurn",
28
+ "SavedSession",
29
+ ]
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Saved-session catalog record
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class SavedSession:
39
+ """One persisted session as the library catalogs it — what a chooser
40
+ shows in a list before anything is opened.
41
+
42
+ ``id`` is the bare session identifier (the filename stem the conductor's
43
+ filesystem backend writes to); ``path`` is its absolute on-disk location.
44
+ The remaining fields are best-effort enrichments filled when the library
45
+ is asked to peek inside a file: a derived ``name``, file
46
+ ``lastModified``/``size``, the ``messageCount`` of conversational nodes,
47
+ and a short ``preview`` of the opening user turn. Any of them may be
48
+ ``None`` when only a shallow listing was taken.
49
+ """
50
+
51
+ #: Bare session identifier — the filename stem, no extension or directory.
52
+ id: str
53
+ #: Absolute on-disk path the session persists to.
54
+ path: str
55
+ #: Human-facing label for the session, when one could be derived.
56
+ name: str | None = None
57
+ #: Last-modified wall-clock time of the file, in epoch milliseconds.
58
+ lastModified: float | None = None
59
+ #: Byte size of the session file on disk.
60
+ size: int | None = None
61
+ #: Count of conversational (user/assistant/tool) nodes in the transcript.
62
+ messageCount: int | None = None
63
+ #: Short single-line excerpt of the opening turn, for at-a-glance
64
+ #: recognition.
65
+ preview: str | None = None
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Transcript-tree navigation node
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class BranchNode:
75
+ """One row in the flattened view of a loaded transcript tree.
76
+
77
+ The conductor stores a branchable tree of nodes; a navigator wants a
78
+ single ordered list it can render and select against. Each
79
+ :class:`BranchNode` names its ``id``, its ``parent`` link (so a caller
80
+ can still reconstruct the tree), a render-ready ``label``, the
81
+ indentation ``depth`` from the root, and two flags: ``isLeaf`` for a tip
82
+ of the tree and ``isCurrent`` for the node the active head points at.
83
+ """
84
+
85
+ #: Stable id of the underlying transcript node.
86
+ id: str
87
+ #: Parent node id, or ``None`` for a transcript root.
88
+ parent: str | None
89
+ #: Render-ready, depth-indented one-line label.
90
+ label: str
91
+ #: Distance from the root (a root is depth 0).
92
+ depth: int
93
+ #: True when this node has no children — a tip of the tree.
94
+ isLeaf: bool
95
+ #: True when the active head currently points at this node.
96
+ isCurrent: bool
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Prior-turn fork candidate
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class PriorTurn:
106
+ """A past user turn offered as a fork point.
107
+
108
+ To re-ask or re-edit an earlier prompt, a caller picks one of these and
109
+ the conductor branches the transcript at ``entryId``. ``text`` is the
110
+ full prompt text; ``preview`` is the trimmed single-line form a picker
111
+ renders.
112
+ """
113
+
114
+ #: Id of the transcript node this user turn lives on (the fork target).
115
+ entryId: str
116
+ #: Full text of the user prompt.
117
+ text: str
118
+ #: Trimmed single-line excerpt of ``text``, for display.
119
+ preview: str