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,349 @@
|
|
|
1
|
+
"""ToolInterceptor pipeline — the composable tool-call boundary.
|
|
2
|
+
|
|
3
|
+
This module is the runtime for the per-tool interception path. It replaces
|
|
4
|
+
the old two-part arrangement — a nested wrapper that re-bound a tool's
|
|
5
|
+
``execute`` plus a separate before/after hook runner — with one ordered
|
|
6
|
+
pipeline of :class:`ToolInterceptor` stages folded around a single tool
|
|
7
|
+
execution.
|
|
8
|
+
|
|
9
|
+
The shape of a stage is ``enter`` (before the tool) and ``exit`` (after it),
|
|
10
|
+
either optional. The chain applies them as a reduce:
|
|
11
|
+
|
|
12
|
+
- **enter, forward.** Each matching stage's ``enter`` runs in registration
|
|
13
|
+
order. A stage may inspect the call, rewrite the decoded ``args`` (return
|
|
14
|
+
an :class:`ArgsRewrite`), or block the call outright (return a
|
|
15
|
+
:class:`GateDecision` with ``stop=True``). A block short-circuits: no later
|
|
16
|
+
``enter`` runs, the real tool never executes, and the chain resolves with
|
|
17
|
+
``blocked`` set.
|
|
18
|
+
- **execute, once.** With the final (possibly rewritten) args, the chain
|
|
19
|
+
invokes the real ``execute`` exactly once.
|
|
20
|
+
- **exit, reverse.** Each matching stage's ``exit`` runs in *reverse*
|
|
21
|
+
registration order, so the stage that entered first exits last — it wraps
|
|
22
|
+
the outermost layer around the call (classic onion ordering). A stage may
|
|
23
|
+
rewrite the result (return a replacement :class:`AgentToolResult`); the
|
|
24
|
+
rewritten result threads into the next (earlier-registered) stage's exit.
|
|
25
|
+
|
|
26
|
+
Fault isolation: a raise from any ``enter``/``exit`` stage is converted to an
|
|
27
|
+
:class:`AddonFault` routed to listeners and then treated as a no-op for that
|
|
28
|
+
stage — enter keeps the prior args (and does not block), exit keeps the prior
|
|
29
|
+
result. A stage's bug therefore degrades to "this stage did nothing" rather
|
|
30
|
+
than crashing the tool call. An error *from the real tool* is not swallowed:
|
|
31
|
+
it is offered to the ``exit`` stages (via :attr:`ToolExitContext.error`) and
|
|
32
|
+
then re-raised so the agent loop sees it, unless an exit stage produced a
|
|
33
|
+
replacement result (a deliberate recovery).
|
|
34
|
+
|
|
35
|
+
Matching: a stage's ``match`` is either an exact wire-facing tool name or
|
|
36
|
+
``"*"`` (every tool). :meth:`AddonInterceptorChain.matches` answers whether
|
|
37
|
+
any stage applies to a tool, letting the host skip the pipeline entirely for
|
|
38
|
+
tools no addon touches.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import inspect
|
|
44
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
from ..contract import (
|
|
48
|
+
AddonFault,
|
|
49
|
+
AddonFaultListener,
|
|
50
|
+
AddonId,
|
|
51
|
+
AgentToolResult,
|
|
52
|
+
ArgsRewrite,
|
|
53
|
+
ExecuteFn,
|
|
54
|
+
GateDecision,
|
|
55
|
+
InterceptResult,
|
|
56
|
+
InterceptorStage,
|
|
57
|
+
ToolEnterContext,
|
|
58
|
+
ToolExitContext,
|
|
59
|
+
ToolInterceptor,
|
|
60
|
+
addon_fault,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"AddonInterceptorChain",
|
|
65
|
+
"interceptor",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _maybe_await(value: Any) -> Any:
|
|
70
|
+
"""Resolve a stage return that may be a plain value or an awaitable — the
|
|
71
|
+
Python analogue of TS ``await`` over ``T | Promise<T>``."""
|
|
72
|
+
if inspect.isawaitable(value):
|
|
73
|
+
return await value
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Interceptor chain
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AddonInterceptorChain:
|
|
83
|
+
"""The concrete :class:`InterceptorChain`: an ordered set of
|
|
84
|
+
:class:`ToolInterceptor` stages folded around one tool execution.
|
|
85
|
+
|
|
86
|
+
Build one with :meth:`from_interceptors` over the host's collected
|
|
87
|
+
interceptors (registration order preserved). Per tool call, :meth:`run`
|
|
88
|
+
selects the matching stages, applies their ``enter`` forward (honoring an
|
|
89
|
+
early block), executes the tool, then applies their ``exit`` in reverse.
|
|
90
|
+
Stage faults are routed to :meth:`on_fault` listeners and never crash the
|
|
91
|
+
call.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, interceptors: Sequence[ToolInterceptor]) -> None:
|
|
95
|
+
"""
|
|
96
|
+
:param interceptors: every addon's recorded interceptors, in the order
|
|
97
|
+
the host folded them (registration order = enter order; reverse =
|
|
98
|
+
exit order)
|
|
99
|
+
"""
|
|
100
|
+
# Every registered interceptor, in registration order.
|
|
101
|
+
self._interceptors: tuple[ToolInterceptor, ...] = tuple(interceptors)
|
|
102
|
+
# Live fault listeners, in registration order (dict = ordered set).
|
|
103
|
+
self._fault_listeners: dict[AddonFaultListener, None] = {}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_interceptors(
|
|
107
|
+
cls, interceptors: Sequence[ToolInterceptor]
|
|
108
|
+
) -> AddonInterceptorChain:
|
|
109
|
+
"""Build a chain from a flat list of interceptors.
|
|
110
|
+
|
|
111
|
+
A thin alias for the constructor that reads well at the host's fold
|
|
112
|
+
site (the TS static ``from``; renamed — ``from`` is a Python keyword).
|
|
113
|
+
|
|
114
|
+
:param interceptors: the folded interceptors across all addons
|
|
115
|
+
"""
|
|
116
|
+
return cls(interceptors)
|
|
117
|
+
|
|
118
|
+
async def run(self, ctx: ToolEnterContext, execute: ExecuteFn) -> InterceptResult:
|
|
119
|
+
"""Run a tool's execution through the interceptor pipeline.
|
|
120
|
+
|
|
121
|
+
Selects the stages whose ``match`` admits ``ctx.tool``, applies their
|
|
122
|
+
``enter`` forward (rewriting args or short-circuiting on a block),
|
|
123
|
+
invokes ``execute`` once with the final args, then applies their
|
|
124
|
+
``exit`` in reverse. A blocked enter resolves with ``blocked`` set
|
|
125
|
+
and never calls ``execute``. When no stage matches, ``execute`` runs
|
|
126
|
+
directly with the original args.
|
|
127
|
+
|
|
128
|
+
If the real tool raises, the error is offered to the matching
|
|
129
|
+
``exit`` stages; an exit stage may return a replacement result to
|
|
130
|
+
recover, otherwise the error is re-raised to the caller after the
|
|
131
|
+
exit stages have observed it.
|
|
132
|
+
|
|
133
|
+
:param ctx: the entering call context (tool, call_id, initial args)
|
|
134
|
+
:param execute: the real tool invocation, called with the final args
|
|
135
|
+
"""
|
|
136
|
+
matching = [
|
|
137
|
+
stage for stage in self._interceptors if _matches_tool(stage.match, ctx.tool)
|
|
138
|
+
]
|
|
139
|
+
if len(matching) == 0:
|
|
140
|
+
return InterceptResult(result=await execute(ctx.args))
|
|
141
|
+
|
|
142
|
+
args, blocked = await self._apply_enter(ctx, matching)
|
|
143
|
+
if blocked is not None:
|
|
144
|
+
return InterceptResult(blocked=blocked)
|
|
145
|
+
|
|
146
|
+
exec_ctx = ToolEnterContext(tool=ctx.tool, call_id=ctx.call_id, args=args)
|
|
147
|
+
result: AgentToolResult | None = None
|
|
148
|
+
error: object | None = None
|
|
149
|
+
try:
|
|
150
|
+
result = await execute(exec_ctx.args)
|
|
151
|
+
except Exception as cause:
|
|
152
|
+
error = cause
|
|
153
|
+
|
|
154
|
+
exited = await self._apply_exit(exec_ctx, matching, result, error)
|
|
155
|
+
if exited is not None:
|
|
156
|
+
return InterceptResult(result=exited)
|
|
157
|
+
if error is not None:
|
|
158
|
+
# No exit stage recovered the failure — surface it to the agent loop.
|
|
159
|
+
raise error # type: ignore[misc]
|
|
160
|
+
# A matched call always produces a result here (execute resolved, no error).
|
|
161
|
+
return InterceptResult(result=result)
|
|
162
|
+
|
|
163
|
+
def matches(self, tool: str) -> bool:
|
|
164
|
+
"""Whether any interceptor matches the given tool name."""
|
|
165
|
+
return any(_matches_tool(stage.match, tool) for stage in self._interceptors)
|
|
166
|
+
|
|
167
|
+
def on_fault(self, listener: AddonFaultListener) -> Callable[[], None]:
|
|
168
|
+
"""Register a listener for per-stage :class:`AddonFault` reports.
|
|
169
|
+
|
|
170
|
+
Returns an idempotent unsubscribe thunk; registering the same
|
|
171
|
+
listener twice collapses to one slot.
|
|
172
|
+
|
|
173
|
+
:param listener: the fault sink
|
|
174
|
+
"""
|
|
175
|
+
self._fault_listeners[listener] = None
|
|
176
|
+
active = True
|
|
177
|
+
|
|
178
|
+
def unsubscribe() -> None:
|
|
179
|
+
nonlocal active
|
|
180
|
+
if not active:
|
|
181
|
+
return
|
|
182
|
+
active = False
|
|
183
|
+
self._fault_listeners.pop(listener, None)
|
|
184
|
+
|
|
185
|
+
return unsubscribe
|
|
186
|
+
|
|
187
|
+
async def _apply_enter(
|
|
188
|
+
self,
|
|
189
|
+
ctx: ToolEnterContext,
|
|
190
|
+
stages: Sequence[ToolInterceptor],
|
|
191
|
+
) -> tuple[Mapping[str, Any], GateDecision | None]:
|
|
192
|
+
"""Apply the matching stages' ``enter`` in forward order, folding args
|
|
193
|
+
rewrites and short-circuiting on the first block. A stage that raises
|
|
194
|
+
is isolated and skipped (the prior args are kept, no block)."""
|
|
195
|
+
args = ctx.args
|
|
196
|
+
for stage in stages:
|
|
197
|
+
if stage.enter is None:
|
|
198
|
+
continue
|
|
199
|
+
here = ToolEnterContext(tool=ctx.tool, call_id=ctx.call_id, args=args)
|
|
200
|
+
try:
|
|
201
|
+
outcome = await _maybe_await(stage.enter(here))
|
|
202
|
+
except Exception as cause:
|
|
203
|
+
self._report(
|
|
204
|
+
addon_fault(
|
|
205
|
+
"handler",
|
|
206
|
+
f'Interceptor enter for "{ctx.tool}" threw.',
|
|
207
|
+
addon=stage.addon,
|
|
208
|
+
cause=cause,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
continue
|
|
212
|
+
blocked, rewritten = _read_enter(outcome)
|
|
213
|
+
if blocked is not None:
|
|
214
|
+
return args, blocked
|
|
215
|
+
if rewritten is not None:
|
|
216
|
+
args = rewritten
|
|
217
|
+
return args, None
|
|
218
|
+
|
|
219
|
+
async def _apply_exit(
|
|
220
|
+
self,
|
|
221
|
+
ctx: ToolEnterContext,
|
|
222
|
+
stages: Sequence[ToolInterceptor],
|
|
223
|
+
result: AgentToolResult | None,
|
|
224
|
+
error: object | None,
|
|
225
|
+
) -> AgentToolResult | None:
|
|
226
|
+
"""Apply the matching stages' ``exit`` in reverse order, folding
|
|
227
|
+
result rewrites. Returns the rewritten result when any stage produced
|
|
228
|
+
one (or recovered an error), otherwise ``None`` (the caller keeps the
|
|
229
|
+
original result/error). A stage that raises is isolated and skipped."""
|
|
230
|
+
current = result
|
|
231
|
+
rewritten = False
|
|
232
|
+
for stage in reversed(stages):
|
|
233
|
+
if stage.exit is None:
|
|
234
|
+
continue
|
|
235
|
+
exit_ctx = _build_exit_context(ctx, current, error)
|
|
236
|
+
try:
|
|
237
|
+
outcome = await _maybe_await(stage.exit(exit_ctx))
|
|
238
|
+
except Exception as cause:
|
|
239
|
+
self._report(
|
|
240
|
+
addon_fault(
|
|
241
|
+
"handler",
|
|
242
|
+
f'Interceptor exit for "{ctx.tool}" threw.',
|
|
243
|
+
addon=stage.addon,
|
|
244
|
+
cause=cause,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
continue
|
|
248
|
+
if outcome is not None:
|
|
249
|
+
current = outcome
|
|
250
|
+
rewritten = True
|
|
251
|
+
return current if rewritten else None
|
|
252
|
+
|
|
253
|
+
def _report(self, fault: AddonFault) -> None:
|
|
254
|
+
"""Route a fault to every listener, guarding each listener against its
|
|
255
|
+
own raise."""
|
|
256
|
+
for listener in list(self._fault_listeners):
|
|
257
|
+
try:
|
|
258
|
+
listener(fault)
|
|
259
|
+
except Exception:
|
|
260
|
+
# A throwing fault listener must not break fan-out to the others.
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Outcome reading
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _read_enter(
|
|
270
|
+
outcome: object,
|
|
271
|
+
) -> tuple[GateDecision | None, Mapping[str, Any] | None]:
|
|
272
|
+
"""Interpret an enter outcome into a normalized
|
|
273
|
+
``(blocked, replacement_args)`` verdict — a block decision, a replacement
|
|
274
|
+
args mapping, or neither (continue unchanged).
|
|
275
|
+
|
|
276
|
+
Distinguishes the two contract shapes by type — a :class:`GateDecision`
|
|
277
|
+
carries ``stop``, an :class:`ArgsRewrite` carries ``args`` — and, for TS
|
|
278
|
+
structural-guard parity, also tolerates a plain mapping carrying a bool
|
|
279
|
+
``stop`` key (gate) or a mapping-valued ``args`` key (rewrite). Anything
|
|
280
|
+
else is treated as "leave unchanged".
|
|
281
|
+
"""
|
|
282
|
+
if outcome is None:
|
|
283
|
+
return None, None
|
|
284
|
+
if isinstance(outcome, GateDecision):
|
|
285
|
+
return (outcome if outcome.stop else None), None
|
|
286
|
+
if isinstance(outcome, ArgsRewrite):
|
|
287
|
+
return None, outcome.args
|
|
288
|
+
if isinstance(outcome, Mapping):
|
|
289
|
+
stop = outcome.get("stop")
|
|
290
|
+
if isinstance(stop, bool):
|
|
291
|
+
if not stop:
|
|
292
|
+
return None, None
|
|
293
|
+
raw_reason = outcome.get("reason")
|
|
294
|
+
reason = raw_reason if isinstance(raw_reason, str) else None
|
|
295
|
+
return GateDecision(stop=True, reason=reason), None
|
|
296
|
+
args = outcome.get("args")
|
|
297
|
+
if isinstance(args, Mapping):
|
|
298
|
+
return None, args
|
|
299
|
+
return None, None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _build_exit_context(
|
|
303
|
+
ctx: ToolEnterContext,
|
|
304
|
+
result: AgentToolResult | None,
|
|
305
|
+
error: object | None,
|
|
306
|
+
) -> ToolExitContext:
|
|
307
|
+
"""Build the :class:`ToolExitContext` handed to an exit stage from the
|
|
308
|
+
entering context plus the current result/error. Only sets ``result`` when
|
|
309
|
+
present and ``error`` when present, matching the contract's "one of"
|
|
310
|
+
shape."""
|
|
311
|
+
if error is not None:
|
|
312
|
+
return ToolExitContext(tool=ctx.tool, call_id=ctx.call_id, error=error)
|
|
313
|
+
return ToolExitContext(tool=ctx.tool, call_id=ctx.call_id, result=result)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# Matching
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _matches_tool(match: str, tool: str) -> bool:
|
|
322
|
+
"""Whether an interceptor's ``match`` (an exact name or ``"*"``) admits a
|
|
323
|
+
tool."""
|
|
324
|
+
return match == "*" or match == tool
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Construction helpers
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def interceptor(
|
|
333
|
+
addon: AddonId,
|
|
334
|
+
match: str,
|
|
335
|
+
stage: InterceptorStage,
|
|
336
|
+
) -> ToolInterceptor:
|
|
337
|
+
"""Record one tool interceptor, stamping it with the matched tool and
|
|
338
|
+
owning addon — the shape the :class:`AddonSurface` produces and the chain
|
|
339
|
+
consumes.
|
|
340
|
+
|
|
341
|
+
Small convenience for hosts/tests assembling interceptors by hand without
|
|
342
|
+
going through the full surface builder. The supplied ``stage`` carries
|
|
343
|
+
only the enter/exit hooks; ``match`` and ``addon`` are filled here.
|
|
344
|
+
|
|
345
|
+
:param addon: the addon the interceptor is attributed to
|
|
346
|
+
:param match: the tool name to intercept, or ``"*"`` for every tool
|
|
347
|
+
:param stage: the enter/exit hooks
|
|
348
|
+
"""
|
|
349
|
+
return ToolInterceptor(match=match, addon=addon, enter=stage.enter, exit=stage.exit)
|