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,1035 @@
1
+ """Conductor contract — the FROZEN type surface of the agent runtime core.
2
+
3
+ This module is the single typed seam between the coding-agent *product* (the
4
+ UI/channels that drive a session) and the framework ``Agent`` (the raw LLM
5
+ conversation loop, published by :mod:`indusagi.agent`). It declares *only*
6
+ shapes plus two tiny inert helpers (:func:`conductor_fault` and the pure
7
+ :func:`reduce_state` transition) — no I/O, no orchestration. Every later
8
+ conductor module (the signal hub, the transcript store, the model
9
+ catalog/matcher, the conductor factory, and the :class:`SessionConductor`
10
+ itself) is written against the names declared here, so the file is
11
+ intentionally small, append-mostly, and stable.
12
+
13
+ Design stance (ported from TS ``src/conductor/contract.ts``):
14
+
15
+ - The conductor *wraps* the framework ``Agent``. The framework emits a
16
+ fine-grained ``AgentEvent`` stream for its own loop; the conductor consumes
17
+ that internally and **re-emits a distinct, product-level**
18
+ :data:`SessionSignal` **stream** to consumers. The two are deliberately not
19
+ the same union: ``SessionSignal`` is the stable surface the app renders,
20
+ free to evolve independently of the framework's loop events.
21
+ - Faults are **typed discriminated values** (:class:`ConductorFault`), never
22
+ string sentinels. A consumer switches on ``fault.kind``, not on substring
23
+ matching of a message.
24
+ - Persistence uses a **fresh on-disk vocabulary** (:class:`TranscriptEntry`,
25
+ :class:`SessionHead`, :data:`TRANSCRIPT_SCHEMA`). The node is a
26
+ ``parent``-linked tree, the version is a namespaced string, and the field
27
+ names are the conductor's own — not the framework's session-manager schema.
28
+ - State is exposed as an **immutable snapshot** (:class:`ConductorState`);
29
+ consumers read it, they never mutate it. Every transition runs through the
30
+ pure :func:`reduce_state` reducer, which always returns a brand-new object.
31
+
32
+ Port notes
33
+ ----------
34
+ - TS discriminated unions become frozen ``slots`` dataclasses carrying a
35
+ ``ClassVar`` ``Literal`` tag (``kind`` for signals/faults, ``type`` for
36
+ reducer actions) — the same tag spelling as TS, so a ``match`` on the tag
37
+ reads like the TS ``switch``. Exhaustiveness moved compiler → test (see
38
+ the key-coverage tests in ``tests/conductor``).
39
+ - Field names keep the TS camelCase spelling (``contextTokens``,
40
+ ``createdAt``, ``entryId``, ``modelId`` …) — matching the framework's own
41
+ camelCase dataclasses so the two vocabularies read alike. Functions are
42
+ snake_case.
43
+ - TS ``SignalOf<K>`` (type-level Extract) has no Python analogue; the
44
+ runtime equivalent is an ``isinstance`` check against the variant class,
45
+ and :data:`SIGNAL_KINDS` names the closed kind set for guards/tests.
46
+ - ``reduce_state`` and the ``AgentLike`` / ``CondenseFn`` / ``RetryPolicy``
47
+ seams live in TS ``conductor.ts``; they are *types-and-pure-logic*, so the
48
+ Python port hosts them here in the contract — the wave-2 ``conductor.py``
49
+ (turn loop, queue drain, retry, persist tail) imports them from this
50
+ module.
51
+
52
+ Framework anchors (all from the ``indusagi`` package — the sibling rebuilt
53
+ framework this app targets):
54
+
55
+ - ``AgentEvent``, ``AgentTool``, ``ThinkingLevel`` ← ``indusagi.agent``
56
+ - ``AgentMessage``, ``Model``, ``Usage``, ``KnownProvider`` ← ``indusagi.ai``
57
+ (note: ``AgentMessage`` lives in ``indusagi.ai`` in the Python framework,
58
+ not ``indusagi.agent`` as in TS)
59
+
60
+ The conductor never re-declares these; it composes them.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
66
+ from dataclasses import dataclass, replace
67
+ from typing import Any, ClassVar, Final, Literal, Protocol, TypeAlias
68
+
69
+ from indusagi.agent import AgentEvent, AgentTool, ThinkingLevel
70
+ from indusagi.ai import AgentMessage, KnownProvider, Model, Usage
71
+
72
+ __all__ = [
73
+ "AgentLike",
74
+ "AgentMessage",
75
+ "AgentStateLike",
76
+ "AgentTool",
77
+ "BashOutcome",
78
+ "CompactedSignal",
79
+ "CondenseFn",
80
+ "ConductorFault",
81
+ "ConductorPhase",
82
+ "ConductorState",
83
+ "ExecuteBashOptions",
84
+ "FAULT_KINDS",
85
+ "FaultAction",
86
+ "FaultKind",
87
+ "FaultSignal",
88
+ "HeadAction",
89
+ "IdleSignal",
90
+ "KnownProvider",
91
+ "MatchQuery",
92
+ "Model",
93
+ "ModelAction",
94
+ "ModelCardRef",
95
+ "PersistedSignal",
96
+ "PhaseAction",
97
+ "PromptSignal",
98
+ "QUEUE_MODES",
99
+ "QueueMode",
100
+ "QueueSignal",
101
+ "QueuedInput",
102
+ "RetryPolicy",
103
+ "SIGNAL_KINDS",
104
+ "SessionConductor",
105
+ "SessionConductorOptions",
106
+ "SessionHead",
107
+ "SessionSignal",
108
+ "SessionStats",
109
+ "SettledAction",
110
+ "SignalHandler",
111
+ "SignalKind",
112
+ "StateAction",
113
+ "TRANSCRIPT_ROLES",
114
+ "TRANSCRIPT_SCHEMA",
115
+ "TextSignal",
116
+ "ThinkingLevel",
117
+ "ThinkingSignal",
118
+ "TokenTally",
119
+ "ToolEndSignal",
120
+ "ToolStartSignal",
121
+ "TranscriptEntry",
122
+ "TranscriptRole",
123
+ "TranscriptSchema",
124
+ "TurnEndSignal",
125
+ "Usage",
126
+ "UsageAction",
127
+ "conductor_fault",
128
+ "reduce_state",
129
+ ]
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Faults
134
+ # ---------------------------------------------------------------------------
135
+
136
+ #: The closed set of failure categories the conductor can surface.
137
+ #:
138
+ #: Each is a distinct recovery story, so the kind is a discriminant — not a
139
+ #: free-form string:
140
+ #:
141
+ #: - ``model`` — the LLM call itself failed (transport, provider, decode).
142
+ #: - ``tool`` — a tool invocation threw or returned a hard error.
143
+ #: - ``persistence`` — writing/reading the on-disk transcript failed.
144
+ #: - ``aborted`` — the caller cancelled the in-flight turn via abort.
145
+ #: - ``overflow`` — the context window was exceeded and not condensable.
146
+ FaultKind: TypeAlias = Literal["model", "tool", "persistence", "aborted", "overflow"]
147
+
148
+ #: Every :data:`FaultKind` value, as a frozen tuple for guards and tests.
149
+ FAULT_KINDS: Final[tuple[FaultKind, ...]] = (
150
+ "model",
151
+ "tool",
152
+ "persistence",
153
+ "aborted",
154
+ "overflow",
155
+ )
156
+
157
+
158
+ @dataclass(frozen=True, slots=True)
159
+ class ConductorFault:
160
+ """A typed, discriminated failure value emitted on the
161
+ :data:`SessionSignal` stream and attached to faulted states.
162
+
163
+ ``kind`` selects the category; ``message`` is a human-readable summary;
164
+ the optional ``cause`` carries the underlying error (or any structured
165
+ detail) for logging without forcing consumers to parse the message
166
+ string. It is a *value*, never raised — construct one with
167
+ :func:`conductor_fault`.
168
+ """
169
+
170
+ #: Failure category — the discriminant consumers switch on.
171
+ kind: FaultKind
172
+ #: Human-readable, single-line summary of what went wrong.
173
+ message: str
174
+ #: Underlying error or structured detail, if any (TS optional ``cause``).
175
+ cause: object | None = None
176
+
177
+
178
+ def conductor_fault(
179
+ kind: FaultKind, message: str, cause: object | None = None
180
+ ) -> ConductorFault:
181
+ """Construct a :class:`ConductorFault`. The single sanctioned way to mint
182
+ a fault, so the shape stays uniform across every producer.
183
+
184
+ :param kind: the failure category
185
+ :param message: a human-readable, single-line summary
186
+ :param cause: optional underlying error or structured detail
187
+ """
188
+ return ConductorFault(kind=kind, message=message, cause=cause)
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Product-level signal stream
193
+ # ---------------------------------------------------------------------------
194
+ #
195
+ # The product-level event stream the conductor emits to its consumers (the
196
+ # interactive UI, the print/JSON mode, the JSON-RPC link). This is the
197
+ # conductor's **re-emitted surface** — distinct from the framework
198
+ # ``AgentEvent`` union. The conductor subscribes to the raw framework loop,
199
+ # layers persistence / auto-condense / fault handling on top, and projects the
200
+ # result down to this small, stable set of discriminated signals. Consumers
201
+ # switch on ``kind`` and never see a framework loop event directly.
202
+
203
+
204
+ @dataclass(frozen=True, slots=True)
205
+ class PromptSignal:
206
+ """The user's turn was committed to the conversation; ``text`` is the
207
+ submitted prompt. Emitted the instant the turn is accepted (before the
208
+ model replies) so a UI can echo the user message immediately rather than
209
+ waiting for the first assistant token."""
210
+
211
+ kind: ClassVar[Literal["prompt"]] = "prompt"
212
+ text: str
213
+
214
+
215
+ @dataclass(frozen=True, slots=True)
216
+ class TextSignal:
217
+ """A chunk of assistant answer text streamed in."""
218
+
219
+ kind: ClassVar[Literal["text"]] = "text"
220
+ delta: str
221
+
222
+
223
+ @dataclass(frozen=True, slots=True)
224
+ class ThinkingSignal:
225
+ """A chunk of reasoning/thinking text streamed in."""
226
+
227
+ kind: ClassVar[Literal["thinking"]] = "thinking"
228
+ delta: str
229
+
230
+
231
+ @dataclass(frozen=True, slots=True)
232
+ class ToolStartSignal:
233
+ """A tool invocation began (correlate by ``id``)."""
234
+
235
+ kind: ClassVar[Literal["tool_start"]] = "tool_start"
236
+ id: str
237
+ name: str
238
+
239
+
240
+ @dataclass(frozen=True, slots=True)
241
+ class ToolEndSignal:
242
+ """A tool invocation finished (``ok`` = no error)."""
243
+
244
+ kind: ClassVar[Literal["tool_end"]] = "tool_end"
245
+ id: str
246
+ ok: bool
247
+
248
+
249
+ @dataclass(frozen=True, slots=True)
250
+ class TurnEndSignal:
251
+ """The assistant turn settled; ``usage`` reports token spend."""
252
+
253
+ kind: ClassVar[Literal["turn_end"]] = "turn_end"
254
+ usage: Usage
255
+
256
+
257
+ @dataclass(frozen=True, slots=True)
258
+ class PersistedSignal:
259
+ """The latest node was committed to the transcript (``entryId``)."""
260
+
261
+ kind: ClassVar[Literal["persisted"]] = "persisted"
262
+ entryId: str
263
+
264
+
265
+ @dataclass(frozen=True, slots=True)
266
+ class CompactedSignal:
267
+ """The transcript was condensed to fit the context window."""
268
+
269
+ kind: ClassVar[Literal["compacted"]] = "compacted"
270
+
271
+
272
+ @dataclass(frozen=True, slots=True)
273
+ class FaultSignal:
274
+ """A typed :class:`ConductorFault` occurred."""
275
+
276
+ kind: ClassVar[Literal["fault"]] = "fault"
277
+ fault: ConductorFault
278
+
279
+
280
+ @dataclass(frozen=True, slots=True)
281
+ class QueueSignal:
282
+ """The pending-input queue changed; ``count`` is its new depth."""
283
+
284
+ kind: ClassVar[Literal["queue"]] = "queue"
285
+ count: int
286
+
287
+
288
+ @dataclass(frozen=True, slots=True)
289
+ class IdleSignal:
290
+ """The conductor has no in-flight work and is ready for input."""
291
+
292
+ kind: ClassVar[Literal["idle"]] = "idle"
293
+
294
+
295
+ #: The product-level signal union — the conductor's stable consumer surface.
296
+ SessionSignal: TypeAlias = (
297
+ PromptSignal
298
+ | TextSignal
299
+ | ThinkingSignal
300
+ | ToolStartSignal
301
+ | ToolEndSignal
302
+ | TurnEndSignal
303
+ | PersistedSignal
304
+ | CompactedSignal
305
+ | FaultSignal
306
+ | QueueSignal
307
+ | IdleSignal
308
+ )
309
+
310
+ #: The discriminant literals of :data:`SessionSignal`, for filtering/logging.
311
+ SignalKind: TypeAlias = Literal[
312
+ "prompt",
313
+ "text",
314
+ "thinking",
315
+ "tool_start",
316
+ "tool_end",
317
+ "turn_end",
318
+ "persisted",
319
+ "compacted",
320
+ "fault",
321
+ "queue",
322
+ "idle",
323
+ ]
324
+
325
+ #: Every :data:`SignalKind` value, in declaration order (the runtime stand-in
326
+ #: for TS ``SignalOf`` extraction — pair with an ``isinstance`` check).
327
+ SIGNAL_KINDS: Final[tuple[SignalKind, ...]] = (
328
+ "prompt",
329
+ "text",
330
+ "thinking",
331
+ "tool_start",
332
+ "tool_end",
333
+ "turn_end",
334
+ "persisted",
335
+ "compacted",
336
+ "fault",
337
+ "queue",
338
+ "idle",
339
+ )
340
+
341
+ #: A subscriber callback registered with :meth:`SessionConductor.subscribe`.
342
+ SignalHandler: TypeAlias = Callable[[SessionSignal], None]
343
+
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # On-disk transcript schema
347
+ # ---------------------------------------------------------------------------
348
+
349
+ #: The on-disk transcript schema namespace + version.
350
+ #:
351
+ #: A namespaced string (not a bare integer) so the format is self-describing
352
+ #: and can evolve without colliding with any other versioned artifact in the
353
+ #: app. This is deliberately the conductor's own vocabulary.
354
+ TRANSCRIPT_SCHEMA: Final[str] = "indus/transcript@1"
355
+
356
+ #: The literal type of :data:`TRANSCRIPT_SCHEMA`.
357
+ TranscriptSchema: TypeAlias = Literal["indus/transcript@1"]
358
+
359
+ #: The conversational role a :class:`TranscriptEntry` node carries.
360
+ #:
361
+ #: Spans both the LLM-facing turns (``user``/``assistant``/``tool``) and the
362
+ #: conductor's own bookkeeping nodes (``system`` seed, ``condense`` markers,
363
+ #: and ``note`` for app-injected context). Kept open at the product layer so
364
+ #: the transcript can hold more than the framework's message roles.
365
+ TranscriptRole: TypeAlias = Literal["user", "assistant", "tool", "system", "condense", "note"]
366
+
367
+ #: Every :data:`TranscriptRole` value, as a frozen tuple for guards and tests.
368
+ TRANSCRIPT_ROLES: Final[tuple[TranscriptRole, ...]] = (
369
+ "user",
370
+ "assistant",
371
+ "tool",
372
+ "system",
373
+ "condense",
374
+ "note",
375
+ )
376
+
377
+
378
+ @dataclass(frozen=True, slots=True)
379
+ class TranscriptEntry:
380
+ """A single node in the on-disk transcript tree.
381
+
382
+ The transcript is an append-only **tree**: every node names its
383
+ ``parent`` (a root has ``parent = None``), and the active leaf is tracked
384
+ separately in :class:`SessionHead`. Branching is moving the head to an
385
+ earlier node; the next append becomes that node's child. ``content``
386
+ holds the framework ``AgentMessage`` payload so the node round-trips back
387
+ into the agent loop; ``meta`` carries optional, non-LLM annotations
388
+ (labels, condense bookkeeping, model/reasoning markers).
389
+
390
+ Field names are the conductor's own (``parent``, ``createdAt``, ``meta``)
391
+ — not the framework's persistence schema.
392
+ """
393
+
394
+ #: Stable unique node id (e.g. a ULID).
395
+ id: str
396
+ #: Parent node id, or ``None`` for the transcript root.
397
+ parent: str | None
398
+ #: Conversational role of this node.
399
+ role: TranscriptRole
400
+ #: The framework message payload this node persists.
401
+ content: AgentMessage
402
+ #: ISO-8601 creation timestamp.
403
+ createdAt: str
404
+ #: Optional, non-LLM annotations keyed by name.
405
+ meta: Mapping[str, Any] | None = None
406
+
407
+
408
+ @dataclass(frozen=True, slots=True)
409
+ class SessionHead:
410
+ """The head record of a persisted transcript: which session, and where
411
+ its active leaf currently points.
412
+
413
+ The ``leaf`` is the id of the most recently appended (or branched-to)
414
+ node; walking ``parent`` links from ``leaf`` to a root reconstructs the
415
+ active branch. ``None`` means an empty transcript (no nodes yet).
416
+ """
417
+
418
+ #: Stable identifier of the session this transcript belongs to.
419
+ sessionId: str
420
+ #: Id of the active leaf node, or ``None`` for an empty transcript.
421
+ leaf: str | None
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Model catalog / matcher
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ @dataclass(frozen=True, slots=True)
430
+ class ModelCardRef:
431
+ """A lightweight, resolved reference to one model card in the catalog.
432
+
433
+ This is the *display/identity* projection of a framework ``Model`` — the
434
+ minimum a UI needs to list, label, and select a model without holding the
435
+ full model object. The matcher produces these; the conductor resolves the
436
+ chosen one back to a full ``Model`` when it configures the agent.
437
+ """
438
+
439
+ #: Canonical ``"provider/modelId"`` identifier (the catalog key).
440
+ id: str
441
+ #: Owning provider (a :data:`KnownProvider` or any provider string).
442
+ provider: str
443
+ #: Provider-scoped model id (e.g. ``"claude-sonnet-4"``).
444
+ modelId: str
445
+ #: Human-readable display name.
446
+ name: str
447
+ #: Whether this model exposes a reasoning/thinking budget.
448
+ reasoning: bool
449
+
450
+
451
+ @dataclass(frozen=True, slots=True)
452
+ class MatchQuery:
453
+ """A query against the model catalog/matcher.
454
+
455
+ Resolution is a prioritized candidate pipeline: an explicit
456
+ ``provider`` + ``modelId`` pins a single card; otherwise ``pattern`` is
457
+ matched (exact id, ``provider/`` prefix, then glob/fuzzy) and narrowed by
458
+ the optional capability filters. All fields default to ``None`` so an
459
+ empty query means "the default candidate".
460
+ """
461
+
462
+ #: Free-form selector: an id, an alias, or a glob pattern.
463
+ pattern: str | None = None
464
+ #: Restrict candidates to this provider.
465
+ provider: str | None = None
466
+ #: Pin a specific provider-scoped model id (used with ``provider``).
467
+ modelId: str | None = None
468
+ #: Require reasoning/thinking support.
469
+ reasoning: bool | None = None
470
+ #: Require image input support.
471
+ supportsImageInput: bool | None = None
472
+
473
+
474
+ # ---------------------------------------------------------------------------
475
+ # Conductor lifecycle phase & state
476
+ # ---------------------------------------------------------------------------
477
+
478
+ #: The coarse lifecycle phase of the conductor at a point in time.
479
+ #:
480
+ #: - ``idle`` — assembled and ready; no turn in flight.
481
+ #: - ``streaming`` — an assistant turn is producing text/thinking.
482
+ #: - ``tooling`` — a tool invocation is executing mid-turn.
483
+ #: - ``condensing`` — the transcript is being condensed to fit the window.
484
+ #: - ``faulted`` — the last turn ended in a :class:`ConductorFault`.
485
+ ConductorPhase: TypeAlias = Literal["idle", "streaming", "tooling", "condensing", "faulted"]
486
+
487
+
488
+ @dataclass(frozen=True, slots=True)
489
+ class ConductorState:
490
+ """An immutable snapshot of the conductor's observable state.
491
+
492
+ Returned by :meth:`SessionConductor.snapshot` and resolved by
493
+ :meth:`SessionConductor.submit`. It is a value, not a live view: the
494
+ dataclass is frozen and the object reflects the instant it was taken.
495
+ Re-read with a fresh ``snapshot()`` to observe later changes.
496
+ """
497
+
498
+ #: Coarse lifecycle phase at snapshot time.
499
+ phase: ConductorPhase
500
+ #: The active transcript head (session id + current leaf).
501
+ head: SessionHead
502
+ #: Cumulative token/cost spend across the session so far.
503
+ usage: Usage
504
+ #: Tokens occupying the model's context window as of the most recent turn
505
+ #: — the last assistant turn's reported usage, NOT the cumulative session
506
+ #: spend. This is what the footer's ``ctx:%`` divides by the context
507
+ #: window; ``usage.totalTokens`` grows unbounded across turns and would
508
+ #: inflate it.
509
+ contextTokens: int
510
+ #: Canonical id of the model currently bound to the session.
511
+ modelId: str
512
+ #: The fault from the most recent turn, when ``phase`` is ``"faulted"``.
513
+ fault: ConductorFault | None = None
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # Immutable state reducer
518
+ # ---------------------------------------------------------------------------
519
+ #
520
+ # The transitions the reducer understands — fresh vocabulary, all typed.
521
+ # (TS hosts these in conductor.ts; they are pure shapes + a pure function, so
522
+ # the Python port keeps them on the contract for the wave-2 conductor to
523
+ # import.)
524
+
525
+
526
+ @dataclass(frozen=True, slots=True)
527
+ class PhaseAction:
528
+ """Move to a new lifecycle phase."""
529
+
530
+ type: ClassVar[Literal["phase"]] = "phase"
531
+ phase: ConductorPhase
532
+
533
+
534
+ @dataclass(frozen=True, slots=True)
535
+ class HeadAction:
536
+ """Point the state at a new transcript head."""
537
+
538
+ type: ClassVar[Literal["head"]] = "head"
539
+ head: SessionHead
540
+
541
+
542
+ @dataclass(frozen=True, slots=True)
543
+ class UsageAction:
544
+ """Record the cumulative usage *and* the latest turn's context occupancy.
545
+
546
+ ``usage`` is the running cumulative total (grows unbounded across turns);
547
+ ``contextTokens`` is the latest turn's window occupancy and **replaces**
548
+ the prior value rather than accumulating — the ``ctx:%`` fix.
549
+ """
550
+
551
+ type: ClassVar[Literal["usage"]] = "usage"
552
+ usage: Usage
553
+ contextTokens: int
554
+
555
+
556
+ @dataclass(frozen=True, slots=True)
557
+ class ModelAction:
558
+ """Bind a different canonical model id."""
559
+
560
+ type: ClassVar[Literal["model"]] = "model"
561
+ modelId: str
562
+
563
+
564
+ @dataclass(frozen=True, slots=True)
565
+ class FaultAction:
566
+ """Record a fault and flip the phase to ``faulted``."""
567
+
568
+ type: ClassVar[Literal["fault"]] = "fault"
569
+ fault: ConductorFault
570
+
571
+
572
+ @dataclass(frozen=True, slots=True)
573
+ class SettledAction:
574
+ """Turn ended cleanly: clear any prior fault, go idle."""
575
+
576
+ type: ClassVar[Literal["settled"]] = "settled"
577
+
578
+
579
+ #: The reducer's action union.
580
+ StateAction: TypeAlias = (
581
+ PhaseAction | HeadAction | UsageAction | ModelAction | FaultAction | SettledAction
582
+ )
583
+
584
+
585
+ def reduce_state(prev: ConductorState, action: StateAction) -> ConductorState:
586
+ """Pure transition over :class:`ConductorState`.
587
+
588
+ Always returns a brand-new object; the previous state is never mutated.
589
+ ``settled`` clears a stale fault and returns to ``idle``; ``fault``
590
+ records the fault and flips to ``faulted``.
591
+
592
+ :param prev: the state to transition from (left untouched)
593
+ :param action: the typed transition to apply
594
+ :raises ValueError: on an action outside :data:`StateAction` (the TS
595
+ ``switch`` was compiler-exhaustive; the Python port fails loud)
596
+ """
597
+ tag = action.type
598
+ if tag == "phase":
599
+ return replace(prev, phase=action.phase)
600
+ if tag == "head":
601
+ return replace(prev, head=action.head)
602
+ if tag == "usage":
603
+ return replace(prev, usage=action.usage, contextTokens=action.contextTokens)
604
+ if tag == "model":
605
+ return replace(prev, modelId=action.modelId)
606
+ if tag == "fault":
607
+ return replace(prev, phase="faulted", fault=action.fault)
608
+ if tag == "settled":
609
+ return replace(prev, phase="idle", fault=None)
610
+ raise ValueError(f"unknown state action: {tag!r}")
611
+
612
+
613
+ # ---------------------------------------------------------------------------
614
+ # Pending-input queue
615
+ # ---------------------------------------------------------------------------
616
+
617
+ #: How a queued input rejoins the conversation once the active turn settles.
618
+ #:
619
+ #: - ``steer`` — interrupt-style input meant to redirect the agent;
620
+ #: drained ahead of plain follow-ups.
621
+ #: - ``followUp`` — input that simply waits its turn after the current one.
622
+ #:
623
+ #: The conductor enqueues input under one of these modes when
624
+ #: :meth:`SessionConductor.submit` is called while a turn is in flight, then
625
+ #: drains the queue in order. (Deliberately the conductor's own vocabulary —
626
+ #: distinct from the framework's ``indusagi.agent.QueueMode``.)
627
+ QueueMode: TypeAlias = Literal["steer", "followUp"]
628
+
629
+ #: Every :data:`QueueMode` value, as a frozen tuple for guards and tests.
630
+ QUEUE_MODES: Final[tuple[QueueMode, ...]] = ("steer", "followUp")
631
+
632
+
633
+ @dataclass(frozen=True, slots=True)
634
+ class QueuedInput:
635
+ """One entry in the conductor's pending-input queue: the
636
+ :data:`QueueMode` it was filed under and the raw user ``text``. Surfaced
637
+ by :meth:`SessionConductor.pending_inputs` so a UI can render what is
638
+ waiting."""
639
+
640
+ #: How this input will rejoin the conversation when drained.
641
+ mode: QueueMode
642
+ #: The raw user message text held for a later turn.
643
+ text: str
644
+
645
+
646
+ # ---------------------------------------------------------------------------
647
+ # Session statistics
648
+ # ---------------------------------------------------------------------------
649
+
650
+
651
+ @dataclass(frozen=True, slots=True)
652
+ class TokenTally:
653
+ """Cumulative token spend, broken out by category and totalled (the
654
+ anonymous ``tokens`` object on the TS ``SessionStats``)."""
655
+
656
+ input: int
657
+ output: int
658
+ cacheRead: int
659
+ cacheWrite: int
660
+ total: int
661
+
662
+
663
+ @dataclass(frozen=True, slots=True)
664
+ class SessionStats:
665
+ """A point-in-time tally of the active session: message counts by role,
666
+ tool activity, cumulative token spend, and total cost. Computed by
667
+ :meth:`SessionConductor.stats` from the live message list plus the
668
+ running usage carried on :class:`ConductorState`."""
669
+
670
+ #: Identifier of the session these figures describe.
671
+ sessionId: str
672
+ #: Number of user-role messages in the active branch.
673
+ userMessages: int
674
+ #: Number of assistant-role messages in the active branch.
675
+ assistantMessages: int
676
+ #: Number of tool invocations the assistant issued.
677
+ toolCalls: int
678
+ #: Number of tool-result messages produced in reply.
679
+ toolResults: int
680
+ #: Total message count across all roles.
681
+ totalMessages: int
682
+ #: Cumulative token spend, broken out by category and totalled.
683
+ tokens: TokenTally
684
+ #: Cumulative monetary cost of the session so far.
685
+ cost: float
686
+
687
+
688
+ # ---------------------------------------------------------------------------
689
+ # Bash execution
690
+ # ---------------------------------------------------------------------------
691
+
692
+
693
+ @dataclass(frozen=True, slots=True)
694
+ class ExecuteBashOptions:
695
+ """Options for :meth:`SessionConductor.execute_bash`."""
696
+
697
+ #: When ``True``, the command's output is *not* recorded as a transcript
698
+ #: note, so it never re-enters the agent's context.
699
+ excludeFromContext: bool = False
700
+
701
+
702
+ @dataclass(frozen=True, slots=True)
703
+ class BashOutcome:
704
+ """The settled result of :meth:`SessionConductor.execute_bash`."""
705
+
706
+ #: Combined stdout + stderr of the command.
707
+ output: str
708
+ #: Process exit code (``0`` on success; non-zero, or ``1`` on a thrown
709
+ #: error).
710
+ exitCode: int
711
+
712
+
713
+ # ---------------------------------------------------------------------------
714
+ # Injectable agent surface
715
+ # ---------------------------------------------------------------------------
716
+
717
+
718
+ class AgentStateLike(Protocol):
719
+ """The slice of the framework ``AgentState`` the conductor reads
720
+ (message list, bound model, streaming flag). Field names are the
721
+ framework's own camelCase."""
722
+
723
+ @property
724
+ def messages(self) -> Sequence[AgentMessage]: ...
725
+
726
+ @property
727
+ def model(self) -> Any: ...
728
+
729
+ @property
730
+ def isStreaming(self) -> bool: ...
731
+
732
+
733
+ class AgentLike(Protocol):
734
+ """The slice of the framework ``Agent`` the conductor drives.
735
+
736
+ The real ``Agent`` from :mod:`indusagi.agent` satisfies this
737
+ structurally; tests pass a scripted fake exposing the same
738
+ submit/subscribe/event surface with no network. Keeping the dependency to
739
+ a Protocol (not the concrete class) is what makes the conductor
740
+ unit-testable.
741
+
742
+ Port note: TS also declared an optional ``setThinkingLevel?``. A Python
743
+ Protocol cannot mark a method optional, so it is omitted here; the
744
+ conductor probes for ``set_thinking_level`` with ``getattr`` and, when
745
+ the agent lacks it, stores the level and applies it on the next model
746
+ bind — same behavior as TS.
747
+ """
748
+
749
+ def subscribe(self, fn: Callable[[AgentEvent], None]) -> Callable[[], None]:
750
+ """Subscribe to the raw framework event stream; returns an
751
+ unsubscribe thunk."""
752
+ ...
753
+
754
+ async def prompt(self, input: str) -> None:
755
+ """Run one prompt turn to settlement."""
756
+ ...
757
+
758
+ def abort(self) -> None:
759
+ """Cancel the in-flight turn, if any."""
760
+ ...
761
+
762
+ @property
763
+ def state(self) -> AgentStateLike:
764
+ """The agent's current state (message list, bound model, streaming
765
+ flag)."""
766
+ ...
767
+
768
+ def replace_messages(self, messages: Sequence[AgentMessage]) -> None:
769
+ """Replace the agent's message list (used on resume)."""
770
+ ...
771
+
772
+ def set_model(self, model: Any) -> None:
773
+ """Bind a different model for subsequent turns."""
774
+ ...
775
+
776
+ def set_system_prompt(self, prompt: str) -> None:
777
+ """Seed/replace the system prompt."""
778
+ ...
779
+
780
+ def set_tools(self, tools: Sequence[AgentTool]) -> None:
781
+ """Set the tools available to the loop."""
782
+ ...
783
+
784
+
785
+ # ---------------------------------------------------------------------------
786
+ # Condense seam + retry policy
787
+ # ---------------------------------------------------------------------------
788
+
789
+
790
+ class CondenseFn(Protocol):
791
+ """The pluggable condense hook. Given the active branch's messages, it
792
+ returns the (smaller) message list to replace it with — synchronously or
793
+ as an awaitable. The real window-budget engine
794
+ (:mod:`induscode.window_budget`) is *structurally* one of these; the
795
+ default in the wave-2 conductor is an identity no-op.
796
+
797
+ Note: the framework's own ``runtime.memory`` compactor is **never** wired
798
+ in here — the two compaction engines stay distinct (plan rule 6).
799
+ """
800
+
801
+ def __call__(
802
+ self, messages: Sequence[AgentMessage], force: bool = False
803
+ ) -> Sequence[AgentMessage] | Awaitable[Sequence[AgentMessage]]: ...
804
+
805
+
806
+ @dataclass(frozen=True, slots=True)
807
+ class RetryPolicy:
808
+ """Tuning for the transient-fault auto-retry."""
809
+
810
+ #: Maximum retry attempts after the first try (default 2 at assembly).
811
+ maxAttempts: int
812
+ #: Base backoff in ms; doubled per attempt (default 250 at assembly).
813
+ baseDelayMs: int
814
+
815
+
816
+ # ---------------------------------------------------------------------------
817
+ # The conductor
818
+ # ---------------------------------------------------------------------------
819
+
820
+
821
+ @dataclass(frozen=True, slots=True)
822
+ class SessionConductorOptions:
823
+ """Options that configure a :class:`SessionConductor` at assembly time.
824
+
825
+ Only ``modelId`` is required; everything else has a sensible default
826
+ resolved by the conductor factory. The shape is intentionally small —
827
+ richer wiring (MCP, memory, provider routing) is attached by the factory,
828
+ not passed through this surface.
829
+ """
830
+
831
+ #: Canonical id of the model to bind the session to.
832
+ modelId: str
833
+ #: Initial system prompt seeding the conversation.
834
+ system: str | None = None
835
+ #: Tools made available to the agent for this session.
836
+ tools: Sequence[AgentTool] | None = None
837
+ #: Initial reasoning effort for models that support it.
838
+ thinking: ThinkingLevel | None = None
839
+ #: Working directory the session is scoped to (defaults to process cwd).
840
+ workspace: str | None = None
841
+ #: Directory to persist the transcript into. When set, the conductor
842
+ #: backs its transcript store with a filesystem backend rooted here (one
843
+ #: ``<sessionId>.ndjson`` per session) so the conversation survives the
844
+ #: process and can be resumed. ``None`` (or an injected ``store`` dep)
845
+ #: keeps the default in-memory store — nothing is written to disk.
846
+ sessionsDir: str | None = None
847
+ #: Condense the transcript automatically when it nears the window
848
+ #: (default on; ``None`` means "factory default").
849
+ autoCompact: bool | None = None
850
+ #: Resolve the credential for a provider on each call. Threaded to the
851
+ #: framework ``Agent``, which calls it per request so short-lived OAuth
852
+ #: access tokens can be refreshed and providers with no env-var mapping
853
+ #: still authenticate. Returning ``None`` lets the framework fall back to
854
+ #: its own environment lookup. May be sync or async.
855
+ getApiKey: Callable[[str], Awaitable[str | None] | str | None] | None = None
856
+
857
+
858
+ class SessionConductor(Protocol):
859
+ """The conductor of a single coding-agent session.
860
+
861
+ It owns the framework ``Agent``, threads persistence and auto-condense
862
+ through the turn loop, and exposes a small product API: submit input,
863
+ subscribe to the :data:`SessionSignal` stream, abort the in-flight turn,
864
+ read an immutable :class:`ConductorState` snapshot, resume a persisted
865
+ session, and optionally rotate the active model. This is the surface all
866
+ three run modes drive.
867
+
868
+ Port note: TS declared ``cycleModel?`` as optional; the Python Protocol
869
+ includes ``cycle_model`` and assemblies that do not support mid-session
870
+ model changes may leave it unimplemented — callers probe with ``getattr``.
871
+ """
872
+
873
+ async def submit(self, input: str) -> ConductorState:
874
+ """Submit user input as a new turn and run the agent to settle.
875
+
876
+ Streams :data:`SessionSignal` values to subscribers as the turn
877
+ progresses and resolves to the immutable :class:`ConductorState`
878
+ once the turn settles (success or fault).
879
+
880
+ When a turn is already in flight the input is **not** dropped: it is
881
+ handed to :meth:`enqueue` and run automatically as a later turn once
882
+ the current one settles. In that case ``submit`` resolves immediately
883
+ with the current snapshot rather than waiting for the queued turn.
884
+ """
885
+ ...
886
+
887
+ def enqueue(self, input: str, mode: QueueMode = "followUp") -> None:
888
+ """Queue an input to run as a future turn. Used directly, or reached
889
+ via :meth:`submit` when the conductor is busy. Queued items drain in
890
+ order after the active turn settles, each running as its own turn.
891
+ Emits a ``queue`` signal so a UI can reflect the new depth."""
892
+ ...
893
+
894
+ def pending_count(self) -> int:
895
+ """How many inputs are currently waiting in the pending-input
896
+ queue."""
897
+ ...
898
+
899
+ def pending_inputs(self) -> Sequence[QueuedInput]:
900
+ """A read-only view of the queued inputs, oldest first."""
901
+ ...
902
+
903
+ def clear_queue(self) -> None:
904
+ """Discard every queued input. Emits a ``queue`` signal."""
905
+ ...
906
+
907
+ def dequeue_last(self) -> str | None:
908
+ """Remove and return the text of the most-recently queued input, or
909
+ ``None`` when the queue is empty. Lets a UI pop the last entry back
910
+ into its prompt."""
911
+ ...
912
+
913
+ def messages(self) -> Sequence[AgentMessage]:
914
+ """The live transcript messages for the active branch.
915
+
916
+ A read-through onto the wrapped agent's running message list — what
917
+ the interactive UI renders as the conversation. The returned sequence
918
+ is the current contents at call time; re-read to observe later
919
+ turns."""
920
+ ...
921
+
922
+ def model(self) -> Model | None:
923
+ """The full framework ``Model`` object currently bound to the
924
+ session, or ``None`` when none could be resolved. Tracks the active
925
+ selection across :meth:`select_model` / ``cycle_model`` changes."""
926
+ ...
927
+
928
+ def is_busy(self) -> bool:
929
+ """Whether a turn is currently in flight (guards re-entrant
930
+ submit)."""
931
+ ...
932
+
933
+ def available_models(self) -> list[ModelCardRef]:
934
+ """The model catalog entries a picker lists, best-first. Derived from
935
+ the configured model matcher; returns ``[]`` when no matcher is wired
936
+ in."""
937
+ ...
938
+
939
+ def select_model(self, id: str) -> None:
940
+ """Bind a model by canonical id for subsequent turns. The companion
941
+ of ``cycle_model``; both route through the same selection path."""
942
+ ...
943
+
944
+ async def condense(self) -> None:
945
+ """Manually run the same transcript-condense path the auto-compactor
946
+ uses, emitting the existing ``compacted`` signal. Safe to call when
947
+ idle; a no-op when the condense hook returns the branch unchanged."""
948
+ ...
949
+
950
+ async def fork(self, entryId: str) -> None:
951
+ """Branch the transcript from a prior node. A new branch is opened
952
+ whose parent is ``entryId``; the agent's message list is rebound to
953
+ that branch's root→leaf path. The conductor's head advances onto the
954
+ chosen node."""
955
+ ...
956
+
957
+ async def navigate_tree(self, nodeId: str) -> None:
958
+ """Move the active leaf to ``nodeId``, rebuild that branch's
959
+ root→leaf path, and rebind the agent's message list to it. Used to
960
+ walk between existing branches without forking a new one."""
961
+ ...
962
+
963
+ async def execute_bash(
964
+ self, command: str, opts: ExecuteBashOptions | None = None
965
+ ) -> BashOutcome:
966
+ """Run a shell command in the session workspace, returning its
967
+ combined stdout+stderr and exit code. Unless
968
+ ``opts.excludeFromContext`` is set, the output is recorded as a
969
+ transcript note so it re-enters the agent's context. Never raises: a
970
+ spawn/exec failure resolves to a non-zero :class:`BashOutcome`."""
971
+ ...
972
+
973
+ def stats(self) -> SessionStats:
974
+ """A point-in-time :class:`SessionStats` tally for the active
975
+ session."""
976
+ ...
977
+
978
+ def thinking_level(self) -> ThinkingLevel:
979
+ """The reasoning effort currently applied to the session."""
980
+ ...
981
+
982
+ def set_thinking_level(self, level: ThinkingLevel) -> None:
983
+ """Set the reasoning effort for subsequent turns. Applied to the
984
+ agent when it exposes a setter; otherwise stored and applied on the
985
+ next model bind."""
986
+ ...
987
+
988
+ def cycle_thinking_level(self) -> ThinkingLevel:
989
+ """Advance the reasoning effort to the next level in the cycle,
990
+ applying it, and return the newly-selected level."""
991
+ ...
992
+
993
+ def session_name(self) -> str | None:
994
+ """The human-readable session name, or ``None`` when none is set."""
995
+ ...
996
+
997
+ def set_session_name(self, name: str) -> None:
998
+ """Assign a human-readable name to the session."""
999
+ ...
1000
+
1001
+ def subscribe(self, handler: SignalHandler) -> Callable[[], None]:
1002
+ """Register a handler for the :data:`SessionSignal` stream. Returns
1003
+ an unsubscribe function that removes the handler."""
1004
+ ...
1005
+
1006
+ def abort(self) -> None:
1007
+ """Cancel the in-flight turn, if any; emits an ``aborted`` fault
1008
+ signal."""
1009
+ ...
1010
+
1011
+ def snapshot(self) -> ConductorState:
1012
+ """Read an immutable snapshot of the current
1013
+ :class:`ConductorState`."""
1014
+ ...
1015
+
1016
+ async def resume(self, sessionId: str) -> None:
1017
+ """Restore a previously persisted session, replacing the current
1018
+ transcript and rebinding the agent to the restored model/leaf."""
1019
+ ...
1020
+
1021
+ async def new_session(self) -> None:
1022
+ """Abandon the current conversation and start a fresh, empty session.
1023
+
1024
+ Drops the agent's message history, opens a new session id (so later
1025
+ turns persist separately, leaving the prior transcript intact on
1026
+ disk), zeroes the usage tally, clears the pending-input queue and
1027
+ session name, and settles to ``idle``. Emits an ``idle`` signal so a
1028
+ subscribed UI re-renders the now-empty conversation. This is what
1029
+ ``/clear`` (and ``/new``) drive."""
1030
+ ...
1031
+
1032
+ def cycle_model(self, id: str) -> None:
1033
+ """Rotate the active model for subsequent turns. Optional in TS: not
1034
+ every assembly supports mid-session model changes."""
1035
+ ...