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,336 @@
|
|
|
1
|
+
"""The bridge network — mounting external MCP servers into the ledger.
|
|
2
|
+
|
|
3
|
+
This is the side-effecting half of bridge enrollment. The framework's
|
|
4
|
+
:func:`indusagi.interop.mount_protocol_bridge` does the protocol work: it
|
|
5
|
+
connects every configured server, lists each ready endpoint's tools, and hands
|
|
6
|
+
back a :class:`~indusagi.interop.MountedProtocolBridge` whose ``box``
|
|
7
|
+
(:class:`~indusagi.runtime.ToolBox`) advertises every grafted remote tool
|
|
8
|
+
under its qualified ``"<server>__<tool>"`` name through ``descriptors()`` and
|
|
9
|
+
whose shared ``runner`` routes a call back across the owning endpoint.
|
|
10
|
+
|
|
11
|
+
What this module adds on top:
|
|
12
|
+
|
|
13
|
+
- **Adaptation.** A ``ToolBox`` speaks the runtime's descriptor/runner
|
|
14
|
+
contract; the conductor and catalog speak :data:`Capability` (the framework
|
|
15
|
+
``AgentTool``). :func:`bridge_box_to_capabilities` bridges the two, wrapping
|
|
16
|
+
each descriptor + the shared runner into an ``AgentTool`` whose ``execute``
|
|
17
|
+
invokes the runner and projects its opaque outcome onto an
|
|
18
|
+
:class:`AgentToolResult` (a single text block).
|
|
19
|
+
- **Enrollment.** :func:`attach_bridge_capabilities` folds those adapted
|
|
20
|
+
capabilities into a :class:`BridgeLedger` as ``enroll`` events, returning
|
|
21
|
+
the new ledger, the live fleet (for teardown), and the typed status — never
|
|
22
|
+
mutating a shared list. A wholesale mount failure becomes a ``bridge``
|
|
23
|
+
:class:`DeckFault` ON the result, never raised, so a bad MCP configuration
|
|
24
|
+
degrades the deck instead of sinking session bootstrap.
|
|
25
|
+
- **Cataloging.** :func:`bridge_capability_card` re-presents a grafted
|
|
26
|
+
capability as a :class:`CapabilityCard`, so dynamic MCP tools surface in
|
|
27
|
+
help/introspection beside the static catalog rows.
|
|
28
|
+
|
|
29
|
+
Failure isolation comes for free from the fleet: a server that faulted on
|
|
30
|
+
connect contributes no descriptors and is simply absent from the enrollment.
|
|
31
|
+
|
|
32
|
+
Port note — cancellation tokens: the runtime ``ToolRunner.run`` requires a
|
|
33
|
+
``CancelToken`` (the TS ``AbortSignal``); the framework exports no public
|
|
34
|
+
constructor for one, so — like the workspace locator's use of
|
|
35
|
+
``indusagi._internal.env`` — the adapter mints a placeholder from
|
|
36
|
+
``indusagi._internal.cancel`` when the caller passes no signal (the analogue
|
|
37
|
+
of the TS ``new AbortController().signal``).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
from collections.abc import Sequence
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
|
|
46
|
+
from indusagi._internal.cancel import CancelToken
|
|
47
|
+
from indusagi.ai import TextContent
|
|
48
|
+
from indusagi.interop import (
|
|
49
|
+
BridgeConfig,
|
|
50
|
+
FleetStatus,
|
|
51
|
+
ServerConfig,
|
|
52
|
+
ServerFleet,
|
|
53
|
+
is_protocol_fault,
|
|
54
|
+
)
|
|
55
|
+
from indusagi.interop import mount_protocol_bridge as _mount_protocol_bridge
|
|
56
|
+
from indusagi.runtime import ToolBox, ToolCall
|
|
57
|
+
|
|
58
|
+
from ..contract import (
|
|
59
|
+
AgentToolResult,
|
|
60
|
+
AnyCapability,
|
|
61
|
+
CapabilityCard,
|
|
62
|
+
DeckFault,
|
|
63
|
+
Schema,
|
|
64
|
+
capability_id,
|
|
65
|
+
deck_fault,
|
|
66
|
+
)
|
|
67
|
+
from .ledger import BridgeLedger, EnrollRequest, enroll_bridge_card, withdraw_server
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"AttachResult",
|
|
71
|
+
"attach_bridge_capabilities",
|
|
72
|
+
"bridge_box_to_capabilities",
|
|
73
|
+
"bridge_capability_card",
|
|
74
|
+
"bridge_config",
|
|
75
|
+
"detach_bridge",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _server_of_qualified(name: str) -> str:
|
|
80
|
+
"""One descriptor's server id, recovered from its qualified name."""
|
|
81
|
+
sep = name.find("__")
|
|
82
|
+
return name[:sep] if sep > 0 else ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _safe_stringify(value: object) -> str:
|
|
86
|
+
"""JSON-encode a value, degrading to ``str(value)`` if it is not
|
|
87
|
+
serializable (the TS ``JSON.stringify`` try/catch)."""
|
|
88
|
+
try:
|
|
89
|
+
return json.dumps(value, indent=2, ensure_ascii=False)
|
|
90
|
+
except Exception:
|
|
91
|
+
return str(value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _project_outcome(output: object, is_error: bool) -> AgentToolResult:
|
|
95
|
+
"""Project a remote tool's opaque outcome onto an :class:`AgentToolResult`.
|
|
96
|
+
|
|
97
|
+
The bridged ``ToolBox`` runner returns a runtime ``ToolOutcome`` whose
|
|
98
|
+
``output`` is opaque (an MCP server may return text, structured JSON, or a
|
|
99
|
+
mix of content blocks). The capability contract requires content blocks,
|
|
100
|
+
so we render the output into a single text block, preserving plain strings
|
|
101
|
+
verbatim and JSON-encoding anything structured. The ``is_error`` flag
|
|
102
|
+
rides straight through.
|
|
103
|
+
|
|
104
|
+
:param output: the runner's opaque tool output
|
|
105
|
+
:param is_error: whether the remote tool reported its own failure
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(output, str):
|
|
108
|
+
text = output
|
|
109
|
+
elif output is None:
|
|
110
|
+
text = ""
|
|
111
|
+
else:
|
|
112
|
+
text = _safe_stringify(output)
|
|
113
|
+
return AgentToolResult(
|
|
114
|
+
content=(TextContent(text=text),),
|
|
115
|
+
details=output,
|
|
116
|
+
isError=is_error,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Adaptation: ToolBox descriptor/runner → AgentTool capability
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _BridgedCapability:
|
|
126
|
+
"""One grafted remote tool, adapted to the ``AgentTool`` shape.
|
|
127
|
+
|
|
128
|
+
The descriptor's qualified name, description, and parameter schema are
|
|
129
|
+
carried verbatim; ``execute`` defers to the box's shared runner — passing
|
|
130
|
+
the cancel token through and projecting the outcome with
|
|
131
|
+
:func:`_project_outcome`. The runner already knows how to route a call by
|
|
132
|
+
name back to the owning endpoint, so the wrapper stays a thin shim.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
__slots__ = ("name", "label", "description", "parameters", "_runner")
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self, name: str, description: str, parameters: Schema, runner: object
|
|
139
|
+
) -> None:
|
|
140
|
+
self.name = name
|
|
141
|
+
# The model-facing label doubles as the qualified name; it is unique
|
|
142
|
+
# across servers so it serves as a stable display handle too.
|
|
143
|
+
self.label = name
|
|
144
|
+
self.description = description
|
|
145
|
+
# The bridge already normalized the remote schema to the provider-safe
|
|
146
|
+
# subset; we thread it through as the capability's parameters mapping.
|
|
147
|
+
self.parameters = parameters
|
|
148
|
+
self._runner = runner
|
|
149
|
+
|
|
150
|
+
async def execute(
|
|
151
|
+
self,
|
|
152
|
+
tool_call_id: str,
|
|
153
|
+
params: object,
|
|
154
|
+
signal: object = None,
|
|
155
|
+
on_update: object = None,
|
|
156
|
+
) -> AgentToolResult:
|
|
157
|
+
del on_update
|
|
158
|
+
cancel = signal if isinstance(signal, CancelToken) else CancelToken()
|
|
159
|
+
outcome = await self._runner.run( # type: ignore[attr-defined]
|
|
160
|
+
ToolCall(id=tool_call_id, name=self.name, input=params), cancel
|
|
161
|
+
)
|
|
162
|
+
return _project_outcome(outcome.output, outcome.is_error)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def bridge_box_to_capabilities(box: ToolBox) -> list[AnyCapability]:
|
|
166
|
+
"""Adapt every tool a mounted :class:`~indusagi.runtime.ToolBox`
|
|
167
|
+
advertises into a list of :data:`Capability` objects the conductor and
|
|
168
|
+
ledger consume.
|
|
169
|
+
|
|
170
|
+
Each box descriptor becomes one ``AgentTool``: its qualified name,
|
|
171
|
+
description, and parameter schema are carried verbatim, and its
|
|
172
|
+
``execute`` defers to the box's shared ``runner``.
|
|
173
|
+
|
|
174
|
+
:param box: the tool box returned by ``mount_protocol_bridge``
|
|
175
|
+
"""
|
|
176
|
+
runner = box.runner
|
|
177
|
+
return [
|
|
178
|
+
_BridgedCapability(
|
|
179
|
+
name=descriptor.name,
|
|
180
|
+
description=descriptor.description,
|
|
181
|
+
parameters=descriptor.parameters,
|
|
182
|
+
runner=runner,
|
|
183
|
+
)
|
|
184
|
+
for descriptor in box.descriptors()
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def bridge_capability_card(capability: AnyCapability) -> CapabilityCard:
|
|
189
|
+
"""Re-present a grafted bridge capability as a :class:`CapabilityCard`.
|
|
190
|
+
|
|
191
|
+
Lets dynamic MCP tools appear in the same catalog/help surface as the
|
|
192
|
+
static cards: the card's ``build`` simply returns the already-live
|
|
193
|
+
capability (the graft happened at mount time, so there is nothing further
|
|
194
|
+
to construct). The owning server id is recovered from the qualified name
|
|
195
|
+
for the summary.
|
|
196
|
+
|
|
197
|
+
:param capability: a capability produced by
|
|
198
|
+
:func:`bridge_box_to_capabilities`
|
|
199
|
+
"""
|
|
200
|
+
server = _server_of_qualified(capability.name)
|
|
201
|
+
summary = (
|
|
202
|
+
f'{capability.description} (bridged from MCP server "{server}")'
|
|
203
|
+
if server
|
|
204
|
+
else capability.description
|
|
205
|
+
)
|
|
206
|
+
return CapabilityCard(
|
|
207
|
+
id=capability_id(capability.name),
|
|
208
|
+
title=capability.label,
|
|
209
|
+
summary=summary,
|
|
210
|
+
build=lambda _ctx: capability,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Enrollment: mount + fold into the ledger
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
220
|
+
class AttachResult:
|
|
221
|
+
"""The outcome of attaching one or more MCP servers to a ledger.
|
|
222
|
+
|
|
223
|
+
Bundles the new :class:`BridgeLedger` (with every reachable tool
|
|
224
|
+
enrolled), the live :class:`~indusagi.interop.ServerFleet` the caller owns
|
|
225
|
+
and must eventually tear down, the aggregate
|
|
226
|
+
:data:`~indusagi.interop.FleetStatus` for rendering, the count of tools
|
|
227
|
+
grafted, and a non-fatal :class:`DeckFault` when the mount itself failed
|
|
228
|
+
wholesale.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
# The ledger after enrolling every grafted capability.
|
|
232
|
+
ledger: BridgeLedger
|
|
233
|
+
# How many remote tools were enrolled.
|
|
234
|
+
enrolled: int
|
|
235
|
+
# The running fleet of endpoints; caller closes it via `detach_bridge`.
|
|
236
|
+
fleet: ServerFleet | None = None
|
|
237
|
+
# Aggregate per-server health, when a fleet came up.
|
|
238
|
+
status: FleetStatus | None = None
|
|
239
|
+
# A wholesale-mount fault, when connecting the bridge threw.
|
|
240
|
+
fault: DeckFault | None = None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def attach_bridge_capabilities(
|
|
244
|
+
ledger: BridgeLedger, config: BridgeConfig
|
|
245
|
+
) -> AttachResult:
|
|
246
|
+
"""Mount external MCP servers and enroll their tools into the bridge
|
|
247
|
+
ledger.
|
|
248
|
+
|
|
249
|
+
Connects every server in ``config`` through the framework's
|
|
250
|
+
``mount_protocol_bridge``, adapts the resulting box into capabilities, and
|
|
251
|
+
folds each one into ``ledger`` as an ``enroll`` event — yielding a *new*
|
|
252
|
+
ledger (the input is untouched). The live fleet is returned so the caller
|
|
253
|
+
can read status and later tear it down; a faulted server simply
|
|
254
|
+
contributes no tools and is reflected in the status.
|
|
255
|
+
|
|
256
|
+
A wholesale failure (the mount call itself raising) is caught and reported
|
|
257
|
+
as a ``bridge`` :class:`DeckFault` on the result rather than raised, so a
|
|
258
|
+
bad MCP configuration degrades the deck instead of sinking session
|
|
259
|
+
bootstrap.
|
|
260
|
+
|
|
261
|
+
:param ledger: the ledger to enroll into (left untouched)
|
|
262
|
+
:param config: the set of MCP servers to connect and graft
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
mounted = await _mount_protocol_bridge(config)
|
|
266
|
+
except Exception as cause:
|
|
267
|
+
message = (
|
|
268
|
+
f"bridge mount failed ({cause.kind}): {cause.message}"
|
|
269
|
+
if is_protocol_fault(cause)
|
|
270
|
+
else "bridge mount failed"
|
|
271
|
+
)
|
|
272
|
+
return AttachResult(
|
|
273
|
+
ledger=ledger,
|
|
274
|
+
enrolled=0,
|
|
275
|
+
fault=deck_fault("bridge", message, cause),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
capabilities = bridge_box_to_capabilities(mounted.box)
|
|
279
|
+
next_ledger = ledger
|
|
280
|
+
for capability in capabilities:
|
|
281
|
+
next_ledger = enroll_bridge_card(
|
|
282
|
+
next_ledger,
|
|
283
|
+
EnrollRequest(
|
|
284
|
+
capability=capability,
|
|
285
|
+
server=_server_of_qualified(capability.name),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return AttachResult(
|
|
290
|
+
ledger=next_ledger,
|
|
291
|
+
enrolled=len(capabilities),
|
|
292
|
+
fleet=mounted.fleet,
|
|
293
|
+
status=mounted.fleet.status(),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def detach_bridge(
|
|
298
|
+
ledger: BridgeLedger,
|
|
299
|
+
fleet: ServerFleet,
|
|
300
|
+
servers: Sequence[str] | None = None,
|
|
301
|
+
) -> BridgeLedger:
|
|
302
|
+
"""Detach one or more servers: retire their ledger entries and close the
|
|
303
|
+
fleet.
|
|
304
|
+
|
|
305
|
+
Withdraws every named server's live capabilities from ``ledger`` (or, when
|
|
306
|
+
no names are given, every server present in the fleet status) and
|
|
307
|
+
gracefully tears the fleet down (the Python fleet's ``tear_down()``; TS
|
|
308
|
+
spelled it ``tearDown``). Returns the new ledger; the fleet is no longer
|
|
309
|
+
usable after this resolves. The teardown is best-effort — a close that
|
|
310
|
+
throws is swallowed so ledger withdrawal still completes.
|
|
311
|
+
|
|
312
|
+
:param ledger: the current ledger (left untouched)
|
|
313
|
+
:param fleet: the live fleet to close
|
|
314
|
+
:param servers: optional subset of server ids to detach; defaults to all
|
|
315
|
+
"""
|
|
316
|
+
ids = list(servers) if servers is not None else list(fleet.status().keys())
|
|
317
|
+
next_ledger = ledger
|
|
318
|
+
for server in ids:
|
|
319
|
+
next_ledger = withdraw_server(next_ledger, server)
|
|
320
|
+
try:
|
|
321
|
+
await fleet.tear_down()
|
|
322
|
+
except Exception:
|
|
323
|
+
# Best-effort: the ledger already reflects the withdrawal.
|
|
324
|
+
pass
|
|
325
|
+
return next_ledger
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def bridge_config(servers: Sequence[ServerConfig]) -> BridgeConfig:
|
|
329
|
+
"""Convenience: turn a flat list of MCP server descriptions into the
|
|
330
|
+
:class:`~indusagi.interop.BridgeConfig` that
|
|
331
|
+
:func:`attach_bridge_capabilities` expects.
|
|
332
|
+
|
|
333
|
+
:param servers: the per-server transport configurations, in declaration
|
|
334
|
+
order
|
|
335
|
+
"""
|
|
336
|
+
return BridgeConfig(servers=tuple(servers))
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Built-in capability bridge — ONE seam to the framework's native tools.
|
|
2
|
+
|
|
3
|
+
The framework (``indusagi.agent``, the actions surface) already authors its
|
|
4
|
+
file, search, shell, web, process, and checklist tools as fully-formed
|
|
5
|
+
``AgentTool`` objects and ships a ``create_<name>_tool(cwd, options)`` factory
|
|
6
|
+
for each. This module is the single place the deck reaches across that seam:
|
|
7
|
+
it pairs every native factory with a deck :attr:`CapabilityCard.build`-shaped
|
|
8
|
+
closure so the whole framework tool set is re-exposed as :data:`Capability`
|
|
9
|
+
rows in one file rather than in a dozen one-line re-export stubs.
|
|
10
|
+
|
|
11
|
+
What this buys the rest of the deck:
|
|
12
|
+
|
|
13
|
+
- :data:`BUILTIN_BRIDGE` — a single mapping keyed by the wire-facing tool
|
|
14
|
+
name, each value a :data:`BridgeBuilder`-carrying descriptor that mints the
|
|
15
|
+
live capability for a :class:`DeckContext`. The manifest derives its catalog
|
|
16
|
+
rows from this mapping; nothing else hand-maintains a parallel list of
|
|
17
|
+
built-ins.
|
|
18
|
+
- :func:`build_builtin` — resolve-and-build one built-in by id, surfacing a
|
|
19
|
+
typed :class:`DeckFault` on an unknown id or a builder throw.
|
|
20
|
+
- :data:`BUILTIN_PROFILES` — the per-built-in profile membership the
|
|
21
|
+
data-driven provisioner intersects with a requested :data:`DeckProfile`, so
|
|
22
|
+
the survey (observe-only) set falls out of one table instead of a second
|
|
23
|
+
hand-written tool list.
|
|
24
|
+
|
|
25
|
+
Design notes (ported from TS ``src/capability-deck/builtin-bridge.ts``):
|
|
26
|
+
|
|
27
|
+
- The builders thread ``ctx.cwd`` (and, where a factory takes them, the
|
|
28
|
+
framework option bags) into the native factory. They never reimplement the
|
|
29
|
+
tool — the framework owns the read/edit/grep/bash/etc. behavior; the bridge
|
|
30
|
+
only *binds* it to a working context and re-labels it for the catalog.
|
|
31
|
+
- The process and checklist tools are stateful framework singletons exposed
|
|
32
|
+
through their factories; the bridge keeps them framework-backed while
|
|
33
|
+
presenting them as plain capabilities to the deck.
|
|
34
|
+
- All model-facing tool ``name`` strings are the framework's own wire contract
|
|
35
|
+
and are kept verbatim — the model's prompt and the conductor key off them.
|
|
36
|
+
Only the deck-side titles/summaries are the deck's own prose.
|
|
37
|
+
|
|
38
|
+
Port note — checklist wire names follow the *Python* framework
|
|
39
|
+
--------------------------------------------------------------
|
|
40
|
+
The TS lineage's framework named the checklist pair ``todoread`` /
|
|
41
|
+
``todowrite``; the rebuilt Python framework's singletons advertise
|
|
42
|
+
``todo_read`` / ``todo_set`` (verified live against ``indusagi.agent``).
|
|
43
|
+
Because the bridge's invariant is "ids are the framework wire contract,
|
|
44
|
+
verbatim", the descriptor ids here are ``todo_read`` / ``todo_set`` — the
|
|
45
|
+
names the model actually invokes — not the TS spellings.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
from collections.abc import Callable, Mapping
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from types import MappingProxyType
|
|
53
|
+
|
|
54
|
+
from indusagi.agent import (
|
|
55
|
+
create_bash_tool,
|
|
56
|
+
create_edit_tool,
|
|
57
|
+
create_find_tool,
|
|
58
|
+
create_grep_tool,
|
|
59
|
+
create_ls_tool,
|
|
60
|
+
create_process_tool,
|
|
61
|
+
create_read_tool,
|
|
62
|
+
create_web_fetch_tool,
|
|
63
|
+
create_web_search_tool,
|
|
64
|
+
create_write_tool,
|
|
65
|
+
todo_read_tool,
|
|
66
|
+
todo_write_tool,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
from .contract import (
|
|
70
|
+
AnyCapability,
|
|
71
|
+
Capability,
|
|
72
|
+
CapabilityId,
|
|
73
|
+
CardProfiles,
|
|
74
|
+
DeckContext,
|
|
75
|
+
DeckProfile,
|
|
76
|
+
capability_id,
|
|
77
|
+
deck_fault,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"BUILTIN_BRIDGE",
|
|
82
|
+
"BUILTIN_IDS",
|
|
83
|
+
"BUILTIN_PROFILES",
|
|
84
|
+
"BridgeBuilder",
|
|
85
|
+
"BuiltinDescriptor",
|
|
86
|
+
"build_builtin",
|
|
87
|
+
"build_builtins_for_profile",
|
|
88
|
+
"builtin_descriptors",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Builder seam
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
#: A closure that mints one framework built-in :data:`Capability` for a
|
|
97
|
+
#: working context — the deck's view of a native factory after the bridge has
|
|
98
|
+
#: bound the ``cwd`` (and any option bag) the factory needs.
|
|
99
|
+
#:
|
|
100
|
+
#: Returns :data:`AnyCapability` because the heterogeneous built-ins carry
|
|
101
|
+
#: different parameter schemas; in Python the open and erased forms collapse
|
|
102
|
+
#: to the same structural ``AgentTool`` Protocol.
|
|
103
|
+
BridgeBuilder = Callable[[DeckContext], AnyCapability]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
107
|
+
class BuiltinDescriptor:
|
|
108
|
+
"""One descriptor in the built-in catalog: the wire-facing id, the
|
|
109
|
+
deck-side title/summary prose, the profiles the tool participates in, and
|
|
110
|
+
the :data:`BridgeBuilder` that binds the framework factory to a context.
|
|
111
|
+
|
|
112
|
+
The manifest turns each of these into a :class:`CapabilityCard`; consumers
|
|
113
|
+
that want the raw built-in set read :data:`BUILTIN_BRIDGE` directly.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
# Wire-facing tool name the model invokes (framework contract, verbatim).
|
|
117
|
+
id: CapabilityId
|
|
118
|
+
# Short human-facing title for catalogs and help text.
|
|
119
|
+
title: str
|
|
120
|
+
# One-line description in the deck's own voice.
|
|
121
|
+
summary: str
|
|
122
|
+
# Which deck profiles this built-in belongs to.
|
|
123
|
+
profiles: CardProfiles
|
|
124
|
+
# Bind the framework factory to a working context.
|
|
125
|
+
build: BridgeBuilder
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Bridge table
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
#: The two profiles that withhold mutation/shell — the observe-only set. A
|
|
133
|
+
#: built-in tagged with both ``authoring`` and ``survey`` is safe to expose
|
|
134
|
+
#: when the agent must not change the workspace; a built-in tagged
|
|
135
|
+
#: ``authoring`` only is a mutating or stateful tool that the survey profile
|
|
136
|
+
#: drops.
|
|
137
|
+
_READ_ONLY: CardProfiles = ("authoring", "survey")
|
|
138
|
+
|
|
139
|
+
#: Mutating / stateful tools: full-access (``authoring``) sessions only.
|
|
140
|
+
_MUTATING: CardProfiles = ("authoring",)
|
|
141
|
+
|
|
142
|
+
#: The single source of truth for the framework built-ins, in catalog order.
|
|
143
|
+
#:
|
|
144
|
+
#: Each row binds a wire name to its framework factory through a
|
|
145
|
+
#: :data:`BridgeBuilder`, declares its profile membership, and carries the
|
|
146
|
+
#: deck-side prose. This list collapses what would otherwise be a stub file
|
|
147
|
+
#: per tool into one table; the manifest's ``CAPABILITY_CARDS`` for built-ins
|
|
148
|
+
#: is a straight projection of it.
|
|
149
|
+
_BUILTIN_DESCRIPTORS: tuple[BuiltinDescriptor, ...] = (
|
|
150
|
+
BuiltinDescriptor(
|
|
151
|
+
id=capability_id("read"),
|
|
152
|
+
title="Read file",
|
|
153
|
+
summary=(
|
|
154
|
+
"Return the contents of a file at a path, optionally from an offset and "
|
|
155
|
+
"capped to a line limit; renders images inline where supported."
|
|
156
|
+
),
|
|
157
|
+
profiles=_READ_ONLY,
|
|
158
|
+
build=lambda ctx: create_read_tool(ctx.cwd),
|
|
159
|
+
),
|
|
160
|
+
BuiltinDescriptor(
|
|
161
|
+
id=capability_id("ls"),
|
|
162
|
+
title="List directory",
|
|
163
|
+
summary=(
|
|
164
|
+
"Enumerate the entries of a directory, with an optional cap on how many "
|
|
165
|
+
"are returned."
|
|
166
|
+
),
|
|
167
|
+
profiles=_READ_ONLY,
|
|
168
|
+
build=lambda ctx: create_ls_tool(ctx.cwd),
|
|
169
|
+
),
|
|
170
|
+
BuiltinDescriptor(
|
|
171
|
+
id=capability_id("grep"),
|
|
172
|
+
title="Search file contents",
|
|
173
|
+
summary=(
|
|
174
|
+
"Scan files for lines matching a pattern, with optional case-insensitivity, "
|
|
175
|
+
"literal matching, surrounding context, and a result cap."
|
|
176
|
+
),
|
|
177
|
+
profiles=_READ_ONLY,
|
|
178
|
+
build=lambda ctx: create_grep_tool(ctx.cwd),
|
|
179
|
+
),
|
|
180
|
+
BuiltinDescriptor(
|
|
181
|
+
id=capability_id("find"),
|
|
182
|
+
title="Find files by name",
|
|
183
|
+
summary=(
|
|
184
|
+
"Locate files and directories whose names match a glob-style pattern "
|
|
185
|
+
"beneath a root, capped to a result limit."
|
|
186
|
+
),
|
|
187
|
+
profiles=_READ_ONLY,
|
|
188
|
+
build=lambda ctx: create_find_tool(ctx.cwd),
|
|
189
|
+
),
|
|
190
|
+
BuiltinDescriptor(
|
|
191
|
+
id=capability_id("websearch"),
|
|
192
|
+
title="Web search",
|
|
193
|
+
summary=(
|
|
194
|
+
"Query the live web for a string and return a ranked set of result "
|
|
195
|
+
"snippets, capped to a requested count."
|
|
196
|
+
),
|
|
197
|
+
profiles=_READ_ONLY,
|
|
198
|
+
build=lambda ctx: create_web_search_tool(),
|
|
199
|
+
),
|
|
200
|
+
BuiltinDescriptor(
|
|
201
|
+
id=capability_id("webfetch"),
|
|
202
|
+
title="Fetch URL",
|
|
203
|
+
summary=(
|
|
204
|
+
"Retrieve a single URL and return its body as text, Markdown, or HTML, "
|
|
205
|
+
"with an optional request timeout."
|
|
206
|
+
),
|
|
207
|
+
profiles=_READ_ONLY,
|
|
208
|
+
build=lambda ctx: create_web_fetch_tool(),
|
|
209
|
+
),
|
|
210
|
+
BuiltinDescriptor(
|
|
211
|
+
id=capability_id("todo_read"),
|
|
212
|
+
title="Read checklist",
|
|
213
|
+
summary=(
|
|
214
|
+
"Read back the session's running task checklist so the agent can review "
|
|
215
|
+
"what is pending, active, or done."
|
|
216
|
+
),
|
|
217
|
+
profiles=_READ_ONLY,
|
|
218
|
+
# The framework's default in-memory checklist singleton; a session-aware
|
|
219
|
+
# persistent store is wired by the separate checklist provisioner, not here.
|
|
220
|
+
build=lambda ctx: todo_read_tool,
|
|
221
|
+
),
|
|
222
|
+
BuiltinDescriptor(
|
|
223
|
+
id=capability_id("write"),
|
|
224
|
+
title="Write file",
|
|
225
|
+
summary=(
|
|
226
|
+
"Create or overwrite a file at a path with the given contents, making "
|
|
227
|
+
"parent directories as needed."
|
|
228
|
+
),
|
|
229
|
+
profiles=_MUTATING,
|
|
230
|
+
build=lambda ctx: create_write_tool(ctx.cwd),
|
|
231
|
+
),
|
|
232
|
+
BuiltinDescriptor(
|
|
233
|
+
id=capability_id("edit"),
|
|
234
|
+
title="Edit file",
|
|
235
|
+
summary=(
|
|
236
|
+
"Apply a find-and-replace edit to a file, swapping an exact span of old "
|
|
237
|
+
"text for new text and reporting the resulting diff."
|
|
238
|
+
),
|
|
239
|
+
profiles=_MUTATING,
|
|
240
|
+
build=lambda ctx: create_edit_tool(ctx.cwd),
|
|
241
|
+
),
|
|
242
|
+
BuiltinDescriptor(
|
|
243
|
+
id=capability_id("bash"),
|
|
244
|
+
title="Run shell command",
|
|
245
|
+
summary=(
|
|
246
|
+
"Execute a shell command in the working directory, streaming its output "
|
|
247
|
+
"and honoring an optional timeout."
|
|
248
|
+
),
|
|
249
|
+
profiles=_MUTATING,
|
|
250
|
+
build=lambda ctx: create_bash_tool(ctx.cwd),
|
|
251
|
+
),
|
|
252
|
+
BuiltinDescriptor(
|
|
253
|
+
id=capability_id("process"),
|
|
254
|
+
title="Manage background processes",
|
|
255
|
+
summary=(
|
|
256
|
+
"Start, list, inspect, feed input to, and stop long-running background "
|
|
257
|
+
"commands without blocking the agent on them."
|
|
258
|
+
),
|
|
259
|
+
profiles=_MUTATING,
|
|
260
|
+
# The TS factory took an option bag ({ cwd }); the Python factory
|
|
261
|
+
# accepts the same bag and reads `cwd` out of it.
|
|
262
|
+
build=lambda ctx: create_process_tool(options={"cwd": ctx.cwd}),
|
|
263
|
+
),
|
|
264
|
+
BuiltinDescriptor(
|
|
265
|
+
id=capability_id("todo_set"),
|
|
266
|
+
title="Update checklist",
|
|
267
|
+
summary=(
|
|
268
|
+
"Replace or amend the session's task checklist, setting each item's "
|
|
269
|
+
"text, status, and priority."
|
|
270
|
+
),
|
|
271
|
+
profiles=_MUTATING,
|
|
272
|
+
# Paired with the read singleton above; both share the framework default
|
|
273
|
+
# in-memory store. The checklist provisioner swaps in a persistent store.
|
|
274
|
+
build=lambda ctx: todo_write_tool,
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
#: The framework built-ins keyed by their wire-facing :data:`CapabilityId` —
|
|
279
|
+
#: the single mapping the manifest and provisioner read from.
|
|
280
|
+
#:
|
|
281
|
+
#: Derived from the ordered descriptor tuple, so the keyed view and the
|
|
282
|
+
#: ordered list never drift: there is exactly one place a built-in is declared.
|
|
283
|
+
BUILTIN_BRIDGE: Mapping[str, BuiltinDescriptor] = MappingProxyType(
|
|
284
|
+
{descriptor.id: descriptor for descriptor in _BUILTIN_DESCRIPTORS}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
#: The wire-facing ids of every framework built-in, in catalog order.
|
|
288
|
+
#: Convenient for ``--tools`` style selection and for the manifest's
|
|
289
|
+
#: projection.
|
|
290
|
+
BUILTIN_IDS: tuple[CapabilityId, ...] = tuple(
|
|
291
|
+
descriptor.id for descriptor in _BUILTIN_DESCRIPTORS
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
#: The profile membership of every built-in, keyed by id — the slice of the
|
|
295
|
+
#: profile table the data-driven provisioner reads to decide which built-ins a
|
|
296
|
+
#: requested :data:`DeckProfile` includes.
|
|
297
|
+
BUILTIN_PROFILES: Mapping[str, CardProfiles] = MappingProxyType(
|
|
298
|
+
{descriptor.id: descriptor.profiles for descriptor in _BUILTIN_DESCRIPTORS}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Build helpers
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def builtin_descriptors() -> tuple[BuiltinDescriptor, ...]:
|
|
308
|
+
"""The ordered tuple of built-in descriptors — the manifest projects its
|
|
309
|
+
catalog rows from this, preserving order."""
|
|
310
|
+
return _BUILTIN_DESCRIPTORS
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def build_builtin(id: CapabilityId, ctx: DeckContext) -> Capability:
|
|
314
|
+
"""Resolve a built-in by id and build its live capability for a context.
|
|
315
|
+
|
|
316
|
+
Raises a typed :class:`DeckFault`: ``unknown_capability`` when the id is
|
|
317
|
+
not a framework built-in, or ``build_failed`` when the framework factory
|
|
318
|
+
throws. This is the sanctioned entry point so failure shaping stays
|
|
319
|
+
uniform across the deck.
|
|
320
|
+
|
|
321
|
+
:param id: the wire-facing built-in name to build
|
|
322
|
+
:param ctx: the working context to bind the factory to
|
|
323
|
+
"""
|
|
324
|
+
descriptor = BUILTIN_BRIDGE.get(id)
|
|
325
|
+
if descriptor is None:
|
|
326
|
+
raise deck_fault(
|
|
327
|
+
"unknown_capability",
|
|
328
|
+
f'No framework built-in is registered under "{id}".',
|
|
329
|
+
)
|
|
330
|
+
try:
|
|
331
|
+
return descriptor.build(ctx)
|
|
332
|
+
except Exception as cause:
|
|
333
|
+
raise deck_fault(
|
|
334
|
+
"build_failed",
|
|
335
|
+
f'Framework built-in "{id}" failed to build.',
|
|
336
|
+
cause,
|
|
337
|
+
) from cause
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_builtins_for_profile(
|
|
341
|
+
profile: DeckProfile, ctx: DeckContext
|
|
342
|
+
) -> list[AnyCapability]:
|
|
343
|
+
"""Build every framework built-in eligible for a profile, bound to a
|
|
344
|
+
context.
|
|
345
|
+
|
|
346
|
+
Walks the bridge table once, keeps each built-in whose profile membership
|
|
347
|
+
admits the requested profile (``all`` admits everything), and binds it. A
|
|
348
|
+
single data-driven pass replaces a trio of per-profile build functions.
|
|
349
|
+
|
|
350
|
+
:param profile: the requested deck profile
|
|
351
|
+
:param ctx: the working context to bind each factory to
|
|
352
|
+
"""
|
|
353
|
+
built: list[AnyCapability] = []
|
|
354
|
+
for descriptor in _BUILTIN_DESCRIPTORS:
|
|
355
|
+
if profile != "all" and profile not in descriptor.profiles:
|
|
356
|
+
continue
|
|
357
|
+
built.append(build_builtin(descriptor.id, ctx))
|
|
358
|
+
return built
|