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,226 @@
|
|
|
1
|
+
"""Working-memory capability — read and update a persistent scratch note the
|
|
2
|
+
agent carries across turns.
|
|
3
|
+
|
|
4
|
+
Status: minimal in-memory implementation + clearly-typed seam for the
|
|
5
|
+
framework memory subsystem.
|
|
6
|
+
|
|
7
|
+
The framework's ``indusagi.memory`` facade is not yet populated with a public
|
|
8
|
+
working-memory store, so this card ships a self-contained, framework-agnostic
|
|
9
|
+
implementation: a single mutable text buffer the agent overwrites or appends
|
|
10
|
+
to, scoped to one built capability (one session). When the framework exposes a
|
|
11
|
+
persistent memory store, a host adapts it to the :class:`MemoryStore` port and
|
|
12
|
+
injects it via :attr:`DeckContext.framework` under :data:`MEMORY_HANDLE_KEY` —
|
|
13
|
+
the :data:`Capability` surface and the tool's wire contract do not change.
|
|
14
|
+
|
|
15
|
+
TODO(framework-memory): adapt ``indusagi.memory`` once it exports a public
|
|
16
|
+
working-memory store; read it from ``ctx.framework[MEMORY_HANDLE_KEY]``.
|
|
17
|
+
|
|
18
|
+
The single tool keys behavior on an ``action`` discriminant:
|
|
19
|
+
|
|
20
|
+
- ``read`` — return the current working-memory note.
|
|
21
|
+
- ``replace`` — overwrite the note with the supplied ``content``.
|
|
22
|
+
- ``append`` — add a line to the end of the note.
|
|
23
|
+
|
|
24
|
+
Port note: the TypeBox schema becomes a dict-literal JSON Schema; an unknown
|
|
25
|
+
``action`` yields a defensive ``isError`` result rather than relying on
|
|
26
|
+
upstream validation.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Literal, Protocol
|
|
34
|
+
|
|
35
|
+
from indusagi.agent import AgentToolResult
|
|
36
|
+
from indusagi.ai import TextContent
|
|
37
|
+
|
|
38
|
+
from ..contract import Capability, CapabilityCard, DeckContext, Schema, capability_id
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"InMemoryStore",
|
|
42
|
+
"MEMORY_HANDLE_KEY",
|
|
43
|
+
"MemoryDetails",
|
|
44
|
+
"MemoryStore",
|
|
45
|
+
"build_memory_capability",
|
|
46
|
+
"memory_card",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Store (in-memory, card-owned) + injection seam
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
#: Key under which a host may wire a framework-backed memory store in later.
|
|
55
|
+
MEMORY_HANDLE_KEY = "memoryStore"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MemoryStore(Protocol):
|
|
59
|
+
"""The narrow port the memory capability binds to.
|
|
60
|
+
|
|
61
|
+
The in-memory default below satisfies it; a future framework-backed store
|
|
62
|
+
can be adapted to the same three methods and injected through the context.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def read(self) -> str: ...
|
|
66
|
+
|
|
67
|
+
def replace(self, content: str) -> None: ...
|
|
68
|
+
|
|
69
|
+
def append(self, line: str) -> None: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InMemoryStore:
|
|
73
|
+
"""A trivial in-process working-memory buffer, one per built capability."""
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self._buffer = ""
|
|
77
|
+
|
|
78
|
+
def read(self) -> str:
|
|
79
|
+
return self._buffer
|
|
80
|
+
|
|
81
|
+
def replace(self, content: str) -> None:
|
|
82
|
+
self._buffer = content
|
|
83
|
+
|
|
84
|
+
def append(self, line: str) -> None:
|
|
85
|
+
self._buffer = line if not self._buffer else f"{self._buffer}\n{line}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_store(ctx: DeckContext) -> MemoryStore:
|
|
89
|
+
handle = (ctx.framework or {}).get(MEMORY_HANDLE_KEY)
|
|
90
|
+
if (
|
|
91
|
+
handle is not None
|
|
92
|
+
and callable(getattr(handle, "read", None))
|
|
93
|
+
and callable(getattr(handle, "replace", None))
|
|
94
|
+
and callable(getattr(handle, "append", None))
|
|
95
|
+
):
|
|
96
|
+
return handle # type: ignore[return-value] — structural check above
|
|
97
|
+
return InMemoryStore()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Parameters (dict-literal JSON Schema)
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
_MEMORY_PARAMS: Schema = {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"action": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["read", "replace", "append"],
|
|
110
|
+
"description": (
|
|
111
|
+
"`read` returns the note; `replace` overwrites it; `append` adds a "
|
|
112
|
+
"line to the end."
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
"content": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": (
|
|
118
|
+
"New note body (for `replace`) or the line to add (for `append`). "
|
|
119
|
+
"Ignored for `read`."
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
"required": ["action"],
|
|
124
|
+
"additionalProperties": False,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
129
|
+
class MemoryDetails:
|
|
130
|
+
"""Structured detail returned alongside the model-facing content."""
|
|
131
|
+
|
|
132
|
+
action: Literal["read", "replace", "append"]
|
|
133
|
+
ok: bool
|
|
134
|
+
# Length of the note after the call, for quick status.
|
|
135
|
+
length: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_MEMORY_DESCRIPTION = (
|
|
139
|
+
"Keep a small working-memory note that persists across turns — durable facts, decisions, "
|
|
140
|
+
"or reminders you want to retain even after older messages scroll out of context. Use "
|
|
141
|
+
'`action:"read"` to recall it, `action:"replace"` to rewrite it from scratch, and '
|
|
142
|
+
'`action:"append"` to add a single line. Keep it concise; it is a scratchpad, not a log.'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
_HEADINGS: Mapping[str, str] = {
|
|
146
|
+
"read": "Working memory:",
|
|
147
|
+
"replace": "Working memory replaced:",
|
|
148
|
+
"append": "Working memory appended:",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _field(params: object, key: str) -> object:
|
|
153
|
+
if isinstance(params, Mapping):
|
|
154
|
+
return params.get(key)
|
|
155
|
+
return getattr(params, key, None)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Capability builder
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _MemoryCapability:
|
|
164
|
+
"""The live working-memory capability — structurally an ``AgentTool``."""
|
|
165
|
+
|
|
166
|
+
name = "memory"
|
|
167
|
+
label = "Working memory"
|
|
168
|
+
description = _MEMORY_DESCRIPTION
|
|
169
|
+
parameters: Schema = _MEMORY_PARAMS
|
|
170
|
+
|
|
171
|
+
def __init__(self, store: MemoryStore) -> None:
|
|
172
|
+
self._store = store
|
|
173
|
+
|
|
174
|
+
async def execute(
|
|
175
|
+
self,
|
|
176
|
+
tool_call_id: str,
|
|
177
|
+
params: object,
|
|
178
|
+
signal: object = None,
|
|
179
|
+
on_update: object = None,
|
|
180
|
+
) -> AgentToolResult:
|
|
181
|
+
del tool_call_id, signal, on_update
|
|
182
|
+
action = _field(params, "action")
|
|
183
|
+
if action not in ("read", "replace", "append"):
|
|
184
|
+
return AgentToolResult(
|
|
185
|
+
content=(
|
|
186
|
+
TextContent(
|
|
187
|
+
text='`action` must be "read", "replace", or "append".'
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
details=None,
|
|
191
|
+
isError=True,
|
|
192
|
+
)
|
|
193
|
+
content = _field(params, "content")
|
|
194
|
+
if action == "replace":
|
|
195
|
+
self._store.replace(content if isinstance(content, str) else "")
|
|
196
|
+
elif action == "append" and isinstance(content, str) and content:
|
|
197
|
+
self._store.append(content)
|
|
198
|
+
note = self._store.read()
|
|
199
|
+
body = "(empty)" if not note else note
|
|
200
|
+
return AgentToolResult(
|
|
201
|
+
content=(TextContent(text=f"{_HEADINGS[action]}\n{body}"),),
|
|
202
|
+
details=MemoryDetails(action=action, ok=True, length=len(note)),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_memory_capability(ctx: DeckContext) -> _MemoryCapability:
|
|
207
|
+
"""Build the working-memory capability, binding it to an injected store
|
|
208
|
+
when one is wired into the context, or a fresh in-memory store otherwise.
|
|
209
|
+
|
|
210
|
+
:param ctx: the deck context; an optional store is read from
|
|
211
|
+
``ctx.framework[MEMORY_HANDLE_KEY]``
|
|
212
|
+
"""
|
|
213
|
+
return _MemoryCapability(_read_store(ctx))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build(ctx: DeckContext) -> Capability:
|
|
217
|
+
return build_memory_capability(ctx)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
#: Catalog row for the working-memory capability.
|
|
221
|
+
memory_card = CapabilityCard(
|
|
222
|
+
id=capability_id("memory"),
|
|
223
|
+
title="Working memory",
|
|
224
|
+
summary="Read and update a persistent scratch note that survives across turns.",
|
|
225
|
+
build=_build,
|
|
226
|
+
)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""SaaS-action capability — a thin wrapper over the framework's connector
|
|
2
|
+
gateway (Composio-style remote tool execution).
|
|
3
|
+
|
|
4
|
+
The heavy lifting (authenticating to a vendor, resolving toolkit scopes,
|
|
5
|
+
executing a remote tool slug) lives in the framework's connectors layer. This
|
|
6
|
+
card does NOT re-implement any of it; it adapts an injected gateway handle to
|
|
7
|
+
the deck's :data:`Capability` shape so the agent can invoke remote SaaS
|
|
8
|
+
actions by slug. When no gateway is wired into the context the card still
|
|
9
|
+
builds and returns a typed stub, so the deck assembles in every environment.
|
|
10
|
+
|
|
11
|
+
The single tool keys behavior on an ``action`` discriminant:
|
|
12
|
+
|
|
13
|
+
- ``discover`` — list executable remote tool slugs (optionally filtered).
|
|
14
|
+
- ``execute`` — run one remote tool slug with arguments.
|
|
15
|
+
|
|
16
|
+
Vendor-defined identifiers (uppercase tool slugs like ``GITHUB_CREATE_ISSUE``)
|
|
17
|
+
are passed through verbatim — they are part of the connector's wire contract.
|
|
18
|
+
|
|
19
|
+
Port note: the TypeBox schema becomes a dict-literal JSON Schema; the
|
|
20
|
+
``slug``-required-for-execute check was already a runtime guard in TS and
|
|
21
|
+
ports as-is.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from collections.abc import Awaitable, Mapping
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Literal, Protocol
|
|
30
|
+
|
|
31
|
+
from indusagi.agent import AgentToolResult
|
|
32
|
+
from indusagi.ai import TextContent
|
|
33
|
+
|
|
34
|
+
from ..contract import Capability, CapabilityCard, DeckContext, Schema, capability_id
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"RemoteExecution",
|
|
38
|
+
"RemoteToolSummary",
|
|
39
|
+
"SAAS_GATEWAY_KEY",
|
|
40
|
+
"SaasDetails",
|
|
41
|
+
"SaasGatewayPort",
|
|
42
|
+
"build_saas_capability",
|
|
43
|
+
"saas_card",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Gateway handle (injected via DeckContext.framework)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
#: Key under which a host wires a live SaaS gateway into the deck context.
|
|
52
|
+
#: (Kept camelCase — it names a context-bag entry, mirroring the TS wiring.)
|
|
53
|
+
SAAS_GATEWAY_KEY = "saasGateway"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
57
|
+
class RemoteToolSummary:
|
|
58
|
+
"""One executable remote tool advertised by the connector."""
|
|
59
|
+
|
|
60
|
+
# Vendor-defined slug, passed through verbatim (e.g. `GITHUB_CREATE_ISSUE`).
|
|
61
|
+
slug: str
|
|
62
|
+
# Human-readable description from the connector, if any.
|
|
63
|
+
description: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
67
|
+
class RemoteExecution:
|
|
68
|
+
"""The outcome of executing one remote tool slug."""
|
|
69
|
+
|
|
70
|
+
ok: bool
|
|
71
|
+
# Connector payload, surfaced to the model as serialized text.
|
|
72
|
+
data: object
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SaasGatewayPort(Protocol):
|
|
76
|
+
"""The slice of a framework SaaS gateway this card adapts.
|
|
77
|
+
|
|
78
|
+
Deliberately narrow — the deck only needs to list and execute remote
|
|
79
|
+
tools. The framework's full gateway (connection lifecycle, scope planning,
|
|
80
|
+
status reporting) is a superset; a host adapts it to this shape when
|
|
81
|
+
wiring it in.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def discover(
|
|
85
|
+
self, filter: Mapping[str, object] | None = None
|
|
86
|
+
) -> Awaitable[list[RemoteToolSummary]]:
|
|
87
|
+
"""List executable remote tools, optionally filtered by ``toolkits``
|
|
88
|
+
or ``query`` entries of the filter mapping."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def execute(
|
|
92
|
+
self, slug: str, args: Mapping[str, object]
|
|
93
|
+
) -> Awaitable[RemoteExecution]:
|
|
94
|
+
"""Execute one remote tool slug with arguments."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _read_gateway(ctx: DeckContext) -> SaasGatewayPort | None:
|
|
99
|
+
handle = (ctx.framework or {}).get(SAAS_GATEWAY_KEY)
|
|
100
|
+
if (
|
|
101
|
+
handle is not None
|
|
102
|
+
and callable(getattr(handle, "discover", None))
|
|
103
|
+
and callable(getattr(handle, "execute", None))
|
|
104
|
+
):
|
|
105
|
+
return handle # type: ignore[return-value] — structural check above
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Parameters (dict-literal JSON Schema)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
_SAAS_PARAMS: Schema = {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {
|
|
116
|
+
"action": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"enum": ["discover", "execute"],
|
|
119
|
+
"description": (
|
|
120
|
+
"`discover` lists available remote tool slugs; `execute` runs one "
|
|
121
|
+
"slug with arguments."
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
"slug": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": (
|
|
127
|
+
"Vendor tool slug to run (e.g. GITHUB_CREATE_ISSUE). Required for "
|
|
128
|
+
"`execute`."
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
"arguments": {
|
|
132
|
+
"type": "object",
|
|
133
|
+
"additionalProperties": True,
|
|
134
|
+
"description": "Arguments object for the remote tool, as the connector expects.",
|
|
135
|
+
},
|
|
136
|
+
"toolkits": {
|
|
137
|
+
"type": "array",
|
|
138
|
+
"items": {"type": "string"},
|
|
139
|
+
"description": "On `discover`, restrict results to these toolkit names.",
|
|
140
|
+
},
|
|
141
|
+
"query": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "On `discover`, free-text filter over available tools.",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
"required": ["action"],
|
|
147
|
+
"additionalProperties": False,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
152
|
+
class SaasDetails:
|
|
153
|
+
"""Structured detail returned alongside the model-facing content."""
|
|
154
|
+
|
|
155
|
+
action: Literal["discover", "execute"]
|
|
156
|
+
ok: bool
|
|
157
|
+
slug: str | None = None
|
|
158
|
+
count: int | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_SAAS_DESCRIPTION = (
|
|
162
|
+
"Discover and run third-party SaaS actions (GitHub, Slack, Gmail, etc.) through the "
|
|
163
|
+
'connector gateway. Use `action:"discover"` to find the right tool slug, then '
|
|
164
|
+
'`action:"execute"` with that `slug` and an `arguments` object to perform the action. '
|
|
165
|
+
"Tool slugs are vendor-defined and case-sensitive; pass them exactly as listed."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
_STUB_NOTE = (
|
|
169
|
+
"The SaaS connector gateway is not configured in this environment, so no remote action "
|
|
170
|
+
"was performed. Configure a connector (and authenticate the relevant toolkit) to enable "
|
|
171
|
+
"these actions."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _field(params: object, key: str) -> object:
|
|
176
|
+
if isinstance(params, Mapping):
|
|
177
|
+
return params.get(key)
|
|
178
|
+
return getattr(params, key, None)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _stringify(data: object) -> str:
|
|
182
|
+
"""``JSON.stringify(data, null, 2)`` with a non-throwing fallback."""
|
|
183
|
+
try:
|
|
184
|
+
return json.dumps(data, indent=2, ensure_ascii=False, default=str)
|
|
185
|
+
except Exception:
|
|
186
|
+
return str(data)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Capability builder
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _SaasCapability:
|
|
195
|
+
"""The live SaaS-action capability — structurally an ``AgentTool``."""
|
|
196
|
+
|
|
197
|
+
name = "saas-action"
|
|
198
|
+
label = "SaaS action"
|
|
199
|
+
description = _SAAS_DESCRIPTION
|
|
200
|
+
parameters: Schema = _SAAS_PARAMS
|
|
201
|
+
|
|
202
|
+
def __init__(self, gateway: SaasGatewayPort | None) -> None:
|
|
203
|
+
self._gateway = gateway
|
|
204
|
+
|
|
205
|
+
async def execute(
|
|
206
|
+
self,
|
|
207
|
+
tool_call_id: str,
|
|
208
|
+
params: object,
|
|
209
|
+
signal: object = None,
|
|
210
|
+
on_update: object = None,
|
|
211
|
+
) -> AgentToolResult:
|
|
212
|
+
del tool_call_id, signal, on_update
|
|
213
|
+
action_raw = _field(params, "action")
|
|
214
|
+
action: Literal["discover", "execute"] = (
|
|
215
|
+
"discover" if action_raw == "discover" else "execute"
|
|
216
|
+
)
|
|
217
|
+
if self._gateway is None:
|
|
218
|
+
return AgentToolResult(
|
|
219
|
+
content=(TextContent(text=_STUB_NOTE),),
|
|
220
|
+
details=SaasDetails(action=action, ok=False),
|
|
221
|
+
isError=True,
|
|
222
|
+
)
|
|
223
|
+
if action_raw == "discover":
|
|
224
|
+
tools = await self._gateway.discover(
|
|
225
|
+
{"toolkits": _field(params, "toolkits"), "query": _field(params, "query")}
|
|
226
|
+
)
|
|
227
|
+
text = (
|
|
228
|
+
"No remote tools matched."
|
|
229
|
+
if not tools
|
|
230
|
+
else "\n".join(
|
|
231
|
+
f"- {t.slug}{f' — {t.description}' if t.description else ''}"
|
|
232
|
+
for t in tools
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return AgentToolResult(
|
|
236
|
+
content=(TextContent(text=text),),
|
|
237
|
+
details=SaasDetails(action="discover", ok=True, count=len(tools)),
|
|
238
|
+
)
|
|
239
|
+
slug = _field(params, "slug")
|
|
240
|
+
if not isinstance(slug, str) or slug == "":
|
|
241
|
+
return AgentToolResult(
|
|
242
|
+
content=(
|
|
243
|
+
TextContent(text="`slug` is required to execute a SaaS action."),
|
|
244
|
+
),
|
|
245
|
+
details=SaasDetails(action="execute", ok=False),
|
|
246
|
+
isError=True,
|
|
247
|
+
)
|
|
248
|
+
arguments = _field(params, "arguments")
|
|
249
|
+
args: Mapping[str, object] = (
|
|
250
|
+
arguments if isinstance(arguments, Mapping) else {}
|
|
251
|
+
)
|
|
252
|
+
result = await self._gateway.execute(slug, args)
|
|
253
|
+
return AgentToolResult(
|
|
254
|
+
content=(TextContent(text=_stringify(result.data)),),
|
|
255
|
+
details=SaasDetails(action="execute", ok=result.ok, slug=slug),
|
|
256
|
+
isError=not result.ok,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_saas_capability(ctx: DeckContext) -> _SaasCapability:
|
|
261
|
+
"""Build the SaaS-action capability, adapting an injected gateway when
|
|
262
|
+
present.
|
|
263
|
+
|
|
264
|
+
:param ctx: the deck context; an optional gateway is read from
|
|
265
|
+
``ctx.framework[SAAS_GATEWAY_KEY]``
|
|
266
|
+
"""
|
|
267
|
+
return _SaasCapability(_read_gateway(ctx))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build(ctx: DeckContext) -> Capability:
|
|
271
|
+
return build_saas_capability(ctx)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
#: Catalog row for the SaaS-action capability.
|
|
275
|
+
saas_card = CapabilityCard(
|
|
276
|
+
id=capability_id("saas-action"),
|
|
277
|
+
title="SaaS action",
|
|
278
|
+
summary="Discover and run third-party SaaS actions through the connector gateway.",
|
|
279
|
+
build=_build,
|
|
280
|
+
)
|