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,450 @@
1
+ """Capability-deck contract — the FROZEN type surface of the tooling layer.
2
+
3
+ This module is the single typed seam between the coding-agent *product* and
4
+ the set of callable capabilities the agent runtime executes — file ops, the
5
+ shell, search, the web, the delegate/subagent action, the checklist, the
6
+ background-process proxy, SaaS connector actions, and dynamically-grafted MCP
7
+ server tools. It declares *only* shapes plus a handful of tiny inert helpers
8
+ (an id brand, a key minter, a fault factory, and a pure ledger reducer) — no
9
+ I/O, no provisioning, no orchestration. Every later deck module (the manifest
10
+ catalog, the builtin bridge, the per-concern provisioners, the bridge network,
11
+ and the assembled :class:`ToolDeck`) is written against the names declared
12
+ here, so the file is intentionally small, append-mostly, and stable.
13
+
14
+ Design stance (ported from TS ``src/capability-deck/contract.ts``):
15
+
16
+ - A :data:`Capability` is exactly the framework ``AgentTool`` shape. The deck
17
+ does not invent a parallel descriptor type; it *manages* AgentTools so the
18
+ conductor can consume the deck's output directly as ``options.tools``.
19
+ - The catalog is a single source of truth: :class:`CapabilityCard` rows live
20
+ in one ``CAPABILITY_CARDS`` tuple (built by the manifest module), and every
21
+ index/lookup is derived *from* that tuple rather than hand-maintained in a
22
+ second parallel map.
23
+ - Profiles replace a trio of near-identical build functions: one data-driven
24
+ provisioner walks a :data:`DeckProfile` (``"authoring"`` | ``"survey"`` |
25
+ ``"all"``) to assemble the right capability set for a session.
26
+ - MCP enrollment is **event-sourced**: a ``BridgeLedger`` is the fold of an
27
+ append-only stream of :class:`BridgeEntry` events (``enroll`` / ``retire``),
28
+ keyed by a content-hash / ULID :data:`BridgeKey` rather than a path digest.
29
+ The current view is a :class:`LedgerSnapshot` reduced from that stream.
30
+
31
+ Port note — TypeBox collapses to plain JSON-schema mappings
32
+ -----------------------------------------------------------
33
+ The TS contract re-exported ``TSchema`` / ``Static`` from ``@sinclair/typebox``
34
+ and parameterized ``Capability<TParameters, TDetails>`` over them. The Python
35
+ framework's ``AgentTool`` Protocol carries ``parameters`` as a plain
36
+ JSON-schema ``Mapping`` and is not generic, so:
37
+
38
+ - :data:`Schema` is the stand-in for ``TSchema`` (a JSON-schema mapping);
39
+ - ``Static`` (a compile-time type computation) has no Python analogue and is
40
+ dropped — runtime guards replace it where the cards need required fields;
41
+ - :data:`Capability` and :data:`AnyCapability` both alias ``AgentTool``
42
+ (the "erased" and "open" forms collapse to one structural Protocol).
43
+
44
+ Framework anchors (all from the ``indusagi`` package — the sibling rebuilt
45
+ framework this app targets):
46
+
47
+ - :class:`AgentTool`, :class:`AgentToolResult` ← ``indusagi.agent``
48
+ - :class:`ToolBox` ← ``indusagi.runtime``
49
+
50
+ The deck never re-declares these; it composes them.
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ from collections.abc import Callable, Mapping, Sequence
56
+ from dataclasses import dataclass
57
+ from types import MappingProxyType
58
+ from typing import Any, Literal, NewType, Protocol, TypeAlias
59
+
60
+ from indusagi.agent import AgentTool, AgentToolResult
61
+ from indusagi.runtime import ToolBox
62
+
63
+ __all__ = [
64
+ "AgentTool",
65
+ "AgentToolResult",
66
+ "AnyCapability",
67
+ "BridgeEntry",
68
+ "BridgeKey",
69
+ "BridgeOp",
70
+ "Capability",
71
+ "CapabilityCard",
72
+ "CapabilityId",
73
+ "CardProfiles",
74
+ "DeckBox",
75
+ "DeckContext",
76
+ "DeckFault",
77
+ "DeckFaultKind",
78
+ "DeckFrameworkHandles",
79
+ "DeckFsBackend",
80
+ "DeckProfile",
81
+ "DeckShellBackend",
82
+ "LedgerSnapshot",
83
+ "Schema",
84
+ "ToolBox",
85
+ "ToolDeck",
86
+ "bridge_key",
87
+ "capability_id",
88
+ "deck_fault",
89
+ "reduce_ledger",
90
+ ]
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Capability
95
+ # ---------------------------------------------------------------------------
96
+
97
+ #: A JSON-schema parameter shape — the Python stand-in for TypeBox ``TSchema``.
98
+ #:
99
+ #: The framework's tools carry their parameter schemas as plain mappings; the
100
+ #: deck threads them (into content keys, into the model's tool listing) but
101
+ #: never interprets them.
102
+ Schema: TypeAlias = Mapping[str, Any]
103
+
104
+ #: A single callable tool the deck manages.
105
+ #:
106
+ #: Deliberately an alias of the framework ``AgentTool`` rather than a fresh
107
+ #: shape: the conductor and the framework agent loop both expect ``AgentTool``
108
+ #: objects, so the deck's whole job is to *assemble* them, not to wrap them in
109
+ #: a parallel descriptor. A ``Capability`` therefore carries the framework
110
+ #: contract verbatim — ``name``, ``label``, ``description``, a JSON-schema
111
+ #: ``parameters`` mapping, and an async ``execute`` — while the deck owns only
112
+ #: the catalog, profiles, and dynamic-bridge layers around it.
113
+ Capability: TypeAlias = AgentTool
114
+
115
+ #: A capability whose parameter/detail types are erased — the element type of
116
+ #: a heterogeneous deck.
117
+ #:
118
+ #: In TS this was ``Capability<TSchema, unknown>``; the Python ``AgentTool``
119
+ #: Protocol is structural and ungeneric, so the open and erased forms collapse
120
+ #: to the same alias. Kept as its own name so deck code reads identically to
121
+ #: the TS lineage (``AnyCapability`` lists are what the conductor consumes as
122
+ #: ``options.tools`` and what :meth:`ToolDeck.tools` / :meth:`ToolDeck.box`
123
+ #: surface).
124
+ AnyCapability: TypeAlias = AgentTool
125
+
126
+ #: String-branded stable identifier for a capability.
127
+ #:
128
+ #: The wire-facing tool ``name`` the model sees (e.g. ``"read"``, ``"bash"``,
129
+ #: ``"composio_execute"``, a qualified ``"<server>__<tool>"``); branded so an
130
+ #: arbitrary string cannot be passed where a vetted capability id is required.
131
+ #: Mint one with :func:`capability_id`.
132
+ CapabilityId = NewType("CapabilityId", str)
133
+
134
+
135
+ def capability_id(raw: str) -> CapabilityId:
136
+ """Brand a raw string as a :data:`CapabilityId`. The single sanctioned way
137
+ to produce one, so id provenance stays uniform across the catalog and
138
+ bridge.
139
+
140
+ :param raw: the wire-facing tool name to brand
141
+ """
142
+ return CapabilityId(raw)
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Catalog
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ @dataclass(frozen=True, slots=True, kw_only=True)
151
+ class CapabilityCard:
152
+ """One row of the static capability catalog — the deck's single source of
153
+ truth.
154
+
155
+ A card is *metadata plus a builder*: it advertises the capability's
156
+ identity (``id``, ``title``, ``summary``) for help/introspection and
157
+ slash-command listings, and carries a :attr:`build` factory that mints the
158
+ live :data:`Capability` for a given :class:`DeckContext`. The catalog
159
+ tuple (``CAPABILITY_CARDS``, owned by the manifest module) is the only
160
+ hand-maintained list; every index, profile membership, and lookup is
161
+ derived from it.
162
+
163
+ ``build`` is pure with respect to the deck: it reads the injected backends
164
+ from the context and returns a configured :data:`Capability`; it performs
165
+ no enrollment and mutates no shared state.
166
+ """
167
+
168
+ # Stable, wire-facing identifier of the capability this card builds.
169
+ id: CapabilityId
170
+ # Short human-facing title for catalogs and help text.
171
+ title: str
172
+ # One-line description of what the capability does, in the deck's own voice.
173
+ summary: str
174
+ # Build the live capability for a working context (pure).
175
+ build: Callable[[DeckContext], Capability]
176
+
177
+
178
+ #: The set of capabilities a card is eligible for, by profile.
179
+ #:
180
+ #: A card declares which :data:`DeckProfile` values it participates in; the
181
+ #: data-driven provisioner intersects this with the requested profile instead
182
+ #: of branching through separate per-profile build functions. ``"all"`` is
183
+ #: implied for every card and need not be listed.
184
+ CardProfiles: TypeAlias = Sequence["DeckProfile"]
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Profiles & context
189
+ # ---------------------------------------------------------------------------
190
+
191
+ #: The named capability sets a session can be provisioned with — the profile
192
+ #: table that replaces a trio of near-identical build functions.
193
+ #:
194
+ #: - ``authoring`` — the full mutating set: read/write/edit/ls + search +
195
+ #: shell + web + checklist + delegate + background processes + SaaS actions.
196
+ #: The default for an interactive coding session.
197
+ #: - ``survey`` — observe-only: read/ls/search/web/checklist. No filesystem
198
+ #: mutation and no shell. Safe when the agent must not change the workspace.
199
+ #: - ``all`` — every registered capability, nothing withheld.
200
+ DeckProfile: TypeAlias = Literal["authoring", "survey", "all"]
201
+
202
+ #: Filesystem backend port a capability may bind to.
203
+ #:
204
+ #: Intentionally opaque at the contract layer: the concrete port shape is
205
+ #: owned by the framework capabilities kernel. The deck only needs to *thread*
206
+ #: it from a host into a card's builder, so the contract treats it as a
207
+ #: branded handle — threaded, not interpreted, here.
208
+ DeckFsBackend = NewType("DeckFsBackend", object)
209
+
210
+ #: Shell backend port a capability may bind to. Opaque for the same reason as
211
+ #: :data:`DeckFsBackend`: a branded handle the contract threads but never
212
+ #: reads.
213
+ DeckShellBackend = NewType("DeckShellBackend", object)
214
+
215
+ #: A bag of opaque framework handles the novel capabilities wire to — the
216
+ #: subagent/delegate manager, the checklist ledger, the background-process
217
+ #: controller, the SaaS gateway. Each is optional and keyed by name; a card
218
+ #: that needs one reads it from here, and the manifest fills in defaults.
219
+ DeckFrameworkHandles: TypeAlias = Mapping[str, object]
220
+
221
+
222
+ @dataclass(frozen=True, slots=True, kw_only=True)
223
+ class DeckContext:
224
+ """The working context handed to :attr:`CapabilityCard.build`.
225
+
226
+ Bundles the workspace root with the injectable backends a capability binds
227
+ to, so capabilities stay framework-agnostic and testable: a test passes
228
+ fake ``fs`` / ``shell`` ports, production passes the framework-backed
229
+ ones. Every field beyond ``cwd`` is optional — the manifest supplies
230
+ framework defaults when a backend is not injected.
231
+ """
232
+
233
+ # Absolute working directory the session's capabilities are scoped to.
234
+ cwd: str
235
+ # Injectable filesystem port; framework backend is the default.
236
+ fs: DeckFsBackend | None = None
237
+ # Injectable shell port; framework backend is the default.
238
+ shell: DeckShellBackend | None = None
239
+ # Opaque framework handles a capability may need (model registry, stores).
240
+ framework: DeckFrameworkHandles | None = None
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Bridge ledger (event-sourced MCP enrollment)
245
+ # ---------------------------------------------------------------------------
246
+
247
+ #: The stable key of an enrolled bridge capability.
248
+ #:
249
+ #: Minted from the capability's identity rather than from a working-directory
250
+ #: digest, so the same external tool grafted from two sessions collapses to
251
+ #: one key and re-enrolling is idempotent. In practice a content hash of the
252
+ #: qualified ``<server>__<tool>`` name (and its schema) or a fresh ULID;
253
+ #: branded so a raw string cannot stand in for a vetted key.
254
+ BridgeKey = NewType("BridgeKey", str)
255
+
256
+
257
+ def bridge_key(raw: str) -> BridgeKey:
258
+ """Brand a raw string as a :data:`BridgeKey`."""
259
+ return BridgeKey(raw)
260
+
261
+
262
+ #: The operations the MCP enrollment stream records.
263
+ #:
264
+ #: - ``enroll`` — a bridge capability became available and was grafted in (an
265
+ #: upsert: re-enrolling the same :data:`BridgeKey` replaces its entry).
266
+ #: - ``retire`` — a bridge capability (or a whole server's set) was withdrawn
267
+ #: (a splice: the keyed entry is removed from the reduced view).
268
+ BridgeOp: TypeAlias = Literal["enroll", "retire"]
269
+
270
+
271
+ @dataclass(frozen=True, slots=True, kw_only=True)
272
+ class BridgeEntry:
273
+ """A single append-only event in the MCP enrollment stream.
274
+
275
+ The ledger is never mutated in place; every change is one of these entries
276
+ appended to an ordered log, and the live view is the fold of that log. An
277
+ ``enroll`` entry carries the grafted :data:`Capability` and the owning
278
+ server; a ``retire`` entry need only name the :data:`BridgeKey` (and
279
+ server) to remove. Field names are the deck's own, not the framework's
280
+ registry schema.
281
+ """
282
+
283
+ # The operation this event records.
284
+ op: BridgeOp
285
+ # Stable key of the bridge capability the event concerns.
286
+ key: BridgeKey
287
+ # Id of the external MCP server that owns the capability.
288
+ server: str
289
+ # The grafted capability — present on `enroll`, omitted on `retire`.
290
+ capability: AnyCapability | None = None
291
+ # Monotonic sequence number; the append order, used to break upsert ties.
292
+ seq: int
293
+ # ISO-8601 timestamp the event was appended.
294
+ at: str
295
+
296
+
297
+ @dataclass(frozen=True, slots=True, kw_only=True)
298
+ class LedgerSnapshot:
299
+ """The reduced, current view of the MCP enrollment ledger.
300
+
301
+ A pure projection of the :class:`BridgeEntry` stream: the set of bridge
302
+ capabilities live *right now*, keyed by :data:`BridgeKey`, alongside the
303
+ highest sequence number folded and a per-server tool count for status
304
+ rendering. Produced by :func:`reduce_ledger`; consumers read it, never
305
+ mutate (both mappings are read-only proxies).
306
+ """
307
+
308
+ # Currently-enrolled bridge capabilities, keyed by their stable key.
309
+ live: Mapping[BridgeKey, AnyCapability]
310
+ # Number of live capabilities grafted from each server, keyed by server id.
311
+ by_server: Mapping[str, int]
312
+ # The highest `BridgeEntry.seq` folded into this snapshot.
313
+ high_water: int
314
+
315
+
316
+ def reduce_ledger(entries: Sequence[BridgeEntry]) -> LedgerSnapshot:
317
+ """Fold an append-only :class:`BridgeEntry` stream into its current
318
+ :class:`LedgerSnapshot`.
319
+
320
+ Pure and total: ``enroll`` upserts the keyed capability (later ``seq``
321
+ wins on a repeated key), ``retire`` removes it, and entries are applied in
322
+ ``seq`` order so the result is independent of input order. This is the
323
+ single sanctioned reducer, so the event-sourced view stays consistent
324
+ across every producer.
325
+
326
+ :param entries: the enrollment events to fold, in any order
327
+ """
328
+ ordered = sorted(entries, key=lambda entry: entry.seq)
329
+ live: dict[BridgeKey, AnyCapability] = {}
330
+ high_water = 0
331
+ for entry in ordered:
332
+ if entry.seq > high_water:
333
+ high_water = entry.seq
334
+ if entry.op == "enroll":
335
+ if entry.capability is not None:
336
+ live[entry.key] = entry.capability
337
+ else:
338
+ live.pop(entry.key, None)
339
+ # Per-server counts are derived from the live set: map each live key back
340
+ # to the server that last touched it, then tally one per live key.
341
+ key_server: dict[BridgeKey, str] = {}
342
+ for entry in ordered:
343
+ key_server[entry.key] = entry.server
344
+ by_server: dict[str, int] = {}
345
+ for key in live:
346
+ server = key_server.get(key)
347
+ if server is None:
348
+ continue
349
+ by_server[server] = by_server.get(server, 0) + 1
350
+ return LedgerSnapshot(
351
+ live=MappingProxyType(live),
352
+ by_server=MappingProxyType(by_server),
353
+ high_water=high_water,
354
+ )
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Tool deck
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ class ToolDeck(Protocol):
363
+ """The assembled capability deck a session runs against.
364
+
365
+ The product of provisioning a :data:`DeckProfile` against a
366
+ :class:`DeckContext`, optionally with bridge capabilities grafted on. It
367
+ exposes exactly two reads: the flat capability list (for inspection,
368
+ naming, and ``--tools`` style selection) and the consumable box the
369
+ conductor wires in.
370
+
371
+ :meth:`box` returns a ``list[AnyCapability]`` — the same ``AgentTool``
372
+ list the conductor's session options accept directly — so a deck drops
373
+ straight into ``SessionConductorOptions.tools``. The :data:`DeckBox` alias
374
+ also admits the framework ``ToolBox`` for hosts that drive the lower-level
375
+ runtime contract instead of the conductor.
376
+ """
377
+
378
+ def tools(self) -> list[AnyCapability]:
379
+ """The flat list of live capabilities, for inspection and selection."""
380
+ ...
381
+
382
+ def box(self) -> DeckBox:
383
+ """The consumable surface the conductor (or runtime) wires in as its
384
+ tools."""
385
+ ...
386
+
387
+
388
+ #: What :meth:`ToolDeck.box` hands to a consumer.
389
+ #:
390
+ #: Primarily a ``list[AnyCapability]`` (= ``AgentTool`` list), which the
391
+ #: conductor accepts verbatim as ``options.tools``. Also admits the framework
392
+ #: :class:`ToolBox` so a host wiring the raw runtime contract — or grafting
393
+ #: MCP tools through the protocol bridge, which yields a ``ToolBox`` — can
394
+ #: consume the deck the same way.
395
+ DeckBox: TypeAlias = "list[AnyCapability] | ToolBox"
396
+
397
+
398
+ # ---------------------------------------------------------------------------
399
+ # Deck faults
400
+ # ---------------------------------------------------------------------------
401
+
402
+ #: The closed set of failure categories the deck can surface during
403
+ #: provisioning or bridge enrollment.
404
+ #:
405
+ #: - ``unknown_capability`` — a requested id is not in the catalog.
406
+ #: - ``build_failed`` — a card's ``build`` threw while minting a
407
+ #: capability.
408
+ #: - ``bridge`` — connecting/listing/grafting an MCP server
409
+ #: failed.
410
+ #: - ``backend`` — a required injected backend was missing or
411
+ #: invalid.
412
+ DeckFaultKind: TypeAlias = Literal["unknown_capability", "build_failed", "bridge", "backend"]
413
+
414
+
415
+ class DeckFault(Exception):
416
+ """A typed, discriminated failure raised by the deck.
417
+
418
+ ``kind`` selects the category; ``message`` is a human-readable summary;
419
+ the optional ``cause`` carries the underlying error for logging without
420
+ forcing consumers to parse the message. Construct one with
421
+ :func:`deck_fault`.
422
+
423
+ Port note: the TS shape was a plain frozen record ``throw``-n as a value;
424
+ Python requires raisables to be exceptions, so the same three fields ride
425
+ on an :class:`Exception` subclass.
426
+ """
427
+
428
+ def __init__(
429
+ self, kind: DeckFaultKind, message: str, cause: object | None = None
430
+ ) -> None:
431
+ super().__init__(message)
432
+ # Failure category — the discriminant consumers switch on.
433
+ self.kind: DeckFaultKind = kind
434
+ # Human-readable, single-line summary of what went wrong.
435
+ self.message: str = message
436
+ # Underlying error or structured detail, if any.
437
+ self.cause: object | None = cause
438
+
439
+
440
+ def deck_fault(
441
+ kind: DeckFaultKind, message: str, cause: object | None = None
442
+ ) -> DeckFault:
443
+ """Construct a :class:`DeckFault`. The single sanctioned way to mint one,
444
+ so the shape stays uniform across every producer.
445
+
446
+ :param kind: the failure category
447
+ :param message: a human-readable, single-line summary
448
+ :param cause: optional underlying error or structured detail
449
+ """
450
+ return DeckFault(kind, message, cause)
@@ -0,0 +1,126 @@
1
+ """Capability catalog — the deck's single source of truth.
2
+
3
+ This module owns :data:`CAPABILITY_CARDS`: the one hand-maintained-in-one-place
4
+ tuple of :class:`CapabilityCard` rows the rest of the deck reads from. Every
5
+ index, lookup, and profile-membership query is *derived* from this tuple, so
6
+ there is never a second parallel map to keep in sync.
7
+
8
+ The catalog is exactly the framework built-ins — read/write/edit/ls/grep/find/
9
+ bash/process/the checklist pair/web search & fetch — projected out of the
10
+ single :data:`BUILTIN_BRIDGE` table in :mod:`.builtin_bridge`. The app-novel
11
+ cards (checklist, daemon proxy, delegate, SaaS actions, working memory) live
12
+ in :mod:`.cards` and are concatenated onto the built-in selection by the
13
+ provisioner's profile table, exactly as the TS lineage did.
14
+
15
+ A :class:`CapabilityCard` is metadata plus a builder: the wire-facing id, the
16
+ deck-side title/summary prose, and a ``build(ctx)`` that mints the live
17
+ :data:`Capability`. The card does not carry profile membership — that lives in
18
+ the profile table the provisioner consults — so the card shape stays the small
19
+ catalog row the contract froze.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Mapping
25
+ from types import MappingProxyType
26
+
27
+ from .builtin_bridge import BuiltinDescriptor, builtin_descriptors
28
+ from .contract import CapabilityCard, CapabilityId, CardProfiles, DeckProfile
29
+
30
+ __all__ = [
31
+ "CAPABILITY_CARDS",
32
+ "CAPABILITY_INDEX",
33
+ "CARD_PROFILES",
34
+ "capability_ids",
35
+ "cards_for_profile",
36
+ "find_card",
37
+ "has_capability",
38
+ ]
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Card projection
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ def _card_of(descriptor: BuiltinDescriptor) -> CapabilityCard:
47
+ """Project one :class:`BuiltinDescriptor` into the contract's
48
+ :class:`CapabilityCard` shape — drop the profile membership (the
49
+ provisioner reads that from the profile table) and keep id, prose, and the
50
+ build closure."""
51
+ return CapabilityCard(
52
+ id=descriptor.id,
53
+ title=descriptor.title,
54
+ summary=descriptor.summary,
55
+ build=descriptor.build,
56
+ )
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # The catalog
61
+ # ---------------------------------------------------------------------------
62
+
63
+ #: The static capability catalog — the deck's single source of truth.
64
+ #:
65
+ #: One ordered tuple of catalog rows, every row a framework built-in projected
66
+ #: from the bridge table. Consumers never hand-build a parallel list — they
67
+ #: read this tuple (or the derived :data:`CAPABILITY_INDEX` /
68
+ #: :data:`CARD_PROFILES` below).
69
+ CAPABILITY_CARDS: tuple[CapabilityCard, ...] = tuple(
70
+ _card_of(descriptor) for descriptor in builtin_descriptors()
71
+ )
72
+
73
+ #: The catalog keyed by wire-facing :data:`CapabilityId`, derived from
74
+ #: :data:`CAPABILITY_CARDS`. Resolves a ``--tools name1,name2`` selection or a
75
+ #: model-named tool to its card in O(1) without a second hand-maintained map.
76
+ CAPABILITY_INDEX: Mapping[CapabilityId, CapabilityCard] = MappingProxyType(
77
+ {card.id: card for card in CAPABILITY_CARDS}
78
+ )
79
+
80
+ #: Profile membership for every catalog row, keyed by id and derived from the
81
+ #: bridge table. The data-driven provisioner intersects this with a requested
82
+ #: profile to assemble a session's capability set — no per-profile build
83
+ #: functions, just a table walk.
84
+ CARD_PROFILES: Mapping[CapabilityId, CardProfiles] = MappingProxyType(
85
+ {descriptor.id: descriptor.profiles for descriptor in builtin_descriptors()}
86
+ )
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Derived lookups
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ def capability_ids() -> list[CapabilityId]:
95
+ """The wire-facing ids of every catalog row, in catalog order."""
96
+ return [card.id for card in CAPABILITY_CARDS]
97
+
98
+
99
+ def has_capability(id: CapabilityId) -> bool:
100
+ """True when a catalog row exists under this id."""
101
+ return id in CAPABILITY_INDEX
102
+
103
+
104
+ def find_card(id: CapabilityId) -> CapabilityCard | None:
105
+ """Fetch a catalog row by id, or ``None`` when the id is unknown."""
106
+ return CAPABILITY_INDEX.get(id)
107
+
108
+
109
+ def cards_for_profile(profile: DeckProfile) -> list[CapabilityCard]:
110
+ """The catalog rows that participate in a profile, in catalog order.
111
+
112
+ ``all`` returns every row; a named profile keeps only the rows whose
113
+ membership (read from :data:`CARD_PROFILES`) admits it. The single place
114
+ profile filtering happens, so the provisioner stays a thin walk over this
115
+ result.
116
+
117
+ :param profile: the requested deck profile
118
+ """
119
+ if profile == "all":
120
+ return list(CAPABILITY_CARDS)
121
+ selected: list[CapabilityCard] = []
122
+ for card in CAPABILITY_CARDS:
123
+ profiles = CARD_PROFILES.get(card.id)
124
+ if profiles is not None and profile in profiles:
125
+ selected.append(card)
126
+ return selected