induscode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,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
|