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
induscode/boot/boot.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Boot orchestrator — the single function the OS entry point calls.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/boot/boot.ts``. :func:`boot` owns the whole launch arc, in
|
|
4
|
+
VERBATIM TS routing order:
|
|
5
|
+
|
|
6
|
+
1. **Credential command** (``signin`` / ``signout`` / ``login`` / ``logout``
|
|
7
|
+
as ``argv[0]``) → :func:`~induscode.launch.run_credential_command` over
|
|
8
|
+
the disk vault; exits.
|
|
9
|
+
2. **Package command** (``install`` / ``remove`` / ``update`` / ``list`` /
|
|
10
|
+
``config`` as ``argv[0]``) →
|
|
11
|
+
:func:`~induscode.launch.run_package_command` over the preference store;
|
|
12
|
+
exits.
|
|
13
|
+
3. **Meta short-circuits**: ``--help`` (usage from the one declarative flag
|
|
14
|
+
table), ``--version``, ``--list-models [filter]`` — all before any stage
|
|
15
|
+
runs or any directory is touched.
|
|
16
|
+
4. **Stage pipeline** :func:`~.stages.run_stages` (immutable context fold).
|
|
17
|
+
5. **Runner dispatch** via :func:`~.runners.select_runner`; the ``finally``
|
|
18
|
+
drains :attr:`~.contract.BootContext.closables` latest-registered first,
|
|
19
|
+
swallowing teardown errors.
|
|
20
|
+
|
|
21
|
+
:func:`boot` itself never raises to its caller for an expected failure — it
|
|
22
|
+
maps problems to a non-zero exit code so the entry point can simply adopt it.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import inspect
|
|
28
|
+
import sys
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from typing import Final
|
|
31
|
+
|
|
32
|
+
from induscode.launch import (
|
|
33
|
+
CatalogFilter,
|
|
34
|
+
format_credential_fault,
|
|
35
|
+
print_model_catalog,
|
|
36
|
+
register_built_in_oauth_providers,
|
|
37
|
+
render_usage,
|
|
38
|
+
run_credential_command,
|
|
39
|
+
run_package_command,
|
|
40
|
+
)
|
|
41
|
+
from induscode.settings import PreferenceStore
|
|
42
|
+
from induscode.workspace import BRAND, VERSION, create_workspace
|
|
43
|
+
|
|
44
|
+
from .auth_vault import create_auth_vault
|
|
45
|
+
from .contract import BootContext
|
|
46
|
+
from .invocation import tokenize_invocation, wants_help, wants_version
|
|
47
|
+
from .runners import select_runner
|
|
48
|
+
from .stages import run_stages
|
|
49
|
+
|
|
50
|
+
__all__ = ["boot"]
|
|
51
|
+
|
|
52
|
+
#: Exit code for a clean run.
|
|
53
|
+
_EXIT_OK: Final[int] = 0
|
|
54
|
+
|
|
55
|
+
#: Exit code for an unexpected failure during boot.
|
|
56
|
+
_EXIT_FAILURE: Final[int] = 1
|
|
57
|
+
|
|
58
|
+
#: Exit code returned when a handled credential command did not complete.
|
|
59
|
+
_EXIT_CREDENTIAL_FAULT: Final[int] = 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _seed_context(argv: Sequence[str]) -> BootContext:
|
|
63
|
+
"""Build the seed :class:`~.contract.BootContext` from the sliced
|
|
64
|
+
``argv``.
|
|
65
|
+
|
|
66
|
+
Resolves the workspace (pure path computation — directories are
|
|
67
|
+
materialised later by the pipeline) and the brand, parses the command
|
|
68
|
+
line once up front so the meta-request check can run before the pipeline,
|
|
69
|
+
and seeds an empty closables list. The pipeline re-parses the invocation
|
|
70
|
+
in its ``build-invocation`` stage; seeding it here keeps the context
|
|
71
|
+
fully-typed and lets ``--help`` / ``--version`` short-circuit without
|
|
72
|
+
running any stage.
|
|
73
|
+
"""
|
|
74
|
+
return BootContext(
|
|
75
|
+
argv=tuple(argv),
|
|
76
|
+
workspace=create_workspace(),
|
|
77
|
+
brand=BRAND,
|
|
78
|
+
invocation=tokenize_invocation(argv),
|
|
79
|
+
closables=[],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _render_help() -> None:
|
|
84
|
+
"""Render the help banner to stdout. Delegates to the launch
|
|
85
|
+
:func:`~induscode.launch.render_usage`, which generates the full option
|
|
86
|
+
reference from the single declarative flag table — so the help text can
|
|
87
|
+
never drift from what the parser accepts."""
|
|
88
|
+
sys.stdout.write(f"{render_usage()}\n")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _render_version() -> None:
|
|
92
|
+
"""Render the version string to stdout: the brand name and the
|
|
93
|
+
single-source :data:`~induscode.workspace.VERSION`."""
|
|
94
|
+
sys.stdout.write(f"{BRAND.name} {VERSION}\n")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _drain_closables(ctx: BootContext) -> None:
|
|
98
|
+
"""Drain every teardown callback, latest-registered first, swallowing
|
|
99
|
+
individual failures so one bad closer cannot mask the run's exit code or
|
|
100
|
+
strand the rest."""
|
|
101
|
+
for close in reversed(list(ctx.closables)):
|
|
102
|
+
try:
|
|
103
|
+
result = close()
|
|
104
|
+
if inspect.isawaitable(result):
|
|
105
|
+
await result
|
|
106
|
+
except Exception:
|
|
107
|
+
# Teardown failures are non-fatal; the exit code already
|
|
108
|
+
# reflects the run.
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _run_credential(ctx: BootContext) -> int | None:
|
|
113
|
+
"""Route a ``signin`` / ``signout`` credential command, if this
|
|
114
|
+
invocation is one.
|
|
115
|
+
|
|
116
|
+
Hands the raw argv to the launch
|
|
117
|
+
:func:`~induscode.launch.run_credential_command` over a disk-backed vault
|
|
118
|
+
scoped to the resolved workspace. Returns the process exit code when the
|
|
119
|
+
command was handled (zero on success, non-zero with a printed fault
|
|
120
|
+
otherwise), or ``None`` when the leading token was not a credential verb
|
|
121
|
+
so the caller proceeds to a normal launch.
|
|
122
|
+
|
|
123
|
+
The OAuth adapter registry is primed explicitly here (never at import
|
|
124
|
+
time) so the OAuth-capable sign-in routes are available.
|
|
125
|
+
"""
|
|
126
|
+
register_built_in_oauth_providers()
|
|
127
|
+
result = await run_credential_command(
|
|
128
|
+
list(ctx.argv),
|
|
129
|
+
vault=create_auth_vault(ctx.workspace.auth_path),
|
|
130
|
+
profile_dir=str(ctx.workspace.profile_dir),
|
|
131
|
+
)
|
|
132
|
+
if not result.handled:
|
|
133
|
+
return None
|
|
134
|
+
if result.fault is not None:
|
|
135
|
+
sys.stderr.write(f"{format_credential_fault(result.fault)}\n")
|
|
136
|
+
return _EXIT_CREDENTIAL_FAULT
|
|
137
|
+
return _EXIT_OK
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _run_package(ctx: BootContext) -> int | None:
|
|
141
|
+
"""Route an ``install`` / ``remove`` / ``update`` / ``list`` / ``config``
|
|
142
|
+
package command, if this invocation is one.
|
|
143
|
+
|
|
144
|
+
Hands the raw argv to the launch
|
|
145
|
+
:func:`~induscode.launch.run_package_command` over a settings store
|
|
146
|
+
resolved from the workspace, so the configured extension-package sources
|
|
147
|
+
persist through the same two-tier store the rest of the app reads.
|
|
148
|
+
Returns the process exit code when the command was handled, or ``None``
|
|
149
|
+
when the leading token was not a package subcommand.
|
|
150
|
+
"""
|
|
151
|
+
result = await run_package_command(
|
|
152
|
+
list(ctx.argv),
|
|
153
|
+
store=PreferenceStore.from_workspace(ctx.workspace, ctx.invocation.cwd),
|
|
154
|
+
)
|
|
155
|
+
if not result.handled:
|
|
156
|
+
return None
|
|
157
|
+
return result.code if result.code is not None else _EXIT_OK
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def boot(argv: Sequence[str]) -> int:
|
|
161
|
+
"""Boot the agent: resolve everything, dispatch to the chosen runner, and
|
|
162
|
+
return the process exit code.
|
|
163
|
+
|
|
164
|
+
:param argv: the arguments the launch was invoked with, already sliced
|
|
165
|
+
(no interpreter / script path)
|
|
166
|
+
:returns: the exit code the process should adopt
|
|
167
|
+
"""
|
|
168
|
+
seeded = _seed_context(argv)
|
|
169
|
+
|
|
170
|
+
# The credential command owns the invocation when the leading token is a
|
|
171
|
+
# signin / signout verb; it falls through (handled: False) otherwise.
|
|
172
|
+
credential = await _run_credential(seeded)
|
|
173
|
+
if credential is not None:
|
|
174
|
+
return credential
|
|
175
|
+
|
|
176
|
+
# The package command owns the invocation when the leading token is an
|
|
177
|
+
# install / remove / update / list / config verb; it falls through
|
|
178
|
+
# otherwise.
|
|
179
|
+
package = await _run_package(seeded)
|
|
180
|
+
if package is not None:
|
|
181
|
+
return package
|
|
182
|
+
|
|
183
|
+
# Meta requests short-circuit before any stage runs or any directory is
|
|
184
|
+
# touched.
|
|
185
|
+
if wants_help(seeded.invocation):
|
|
186
|
+
_render_help()
|
|
187
|
+
return _EXIT_OK
|
|
188
|
+
if wants_version(seeded.invocation):
|
|
189
|
+
_render_version()
|
|
190
|
+
return _EXIT_OK
|
|
191
|
+
# `--list-models [filter]` prints the catalog and exits — no session, no
|
|
192
|
+
# directory side effects — mirroring `--help` / `--version`.
|
|
193
|
+
if seeded.invocation.list_models:
|
|
194
|
+
filter = seeded.invocation.list_models_filter
|
|
195
|
+
print_model_catalog(
|
|
196
|
+
None,
|
|
197
|
+
CatalogFilter(search=filter) if filter is not None else CatalogFilter(),
|
|
198
|
+
)
|
|
199
|
+
return _EXIT_OK
|
|
200
|
+
|
|
201
|
+
ctx = await run_stages(seeded)
|
|
202
|
+
try:
|
|
203
|
+
runner = select_runner(ctx.invocation)
|
|
204
|
+
return await runner.run(ctx)
|
|
205
|
+
except Exception as error: # noqa: BLE001 — expected failures become exit 1
|
|
206
|
+
message = str(error) if str(error) else type(error).__name__
|
|
207
|
+
sys.stderr.write(f"{BRAND.name}: {message}\n")
|
|
208
|
+
return _EXIT_FAILURE
|
|
209
|
+
finally:
|
|
210
|
+
await _drain_closables(ctx)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Boot-layer contract — the FROZEN type surface between the OS entry point
|
|
2
|
+
and everything that turns a command line into a running coding agent.
|
|
3
|
+
|
|
4
|
+
Port of TS ``src/boot/contract.ts``. It declares *only* shapes — no behavior,
|
|
5
|
+
no I/O, no literals beyond the narrow string unions that pin the public modes.
|
|
6
|
+
Every other boot module (stage pipeline, invocation projection, runner
|
|
7
|
+
registry, upgrade driver) is written against the names declared here, so this
|
|
8
|
+
file is intentionally small, append-mostly, and stable.
|
|
9
|
+
|
|
10
|
+
Design stance (kept from TS):
|
|
11
|
+
|
|
12
|
+
- One immutable :class:`BootContext` is threaded through an ordered list of
|
|
13
|
+
:class:`Stage` transforms; a stage returns the next context (or the same
|
|
14
|
+
one) and never mutates in place. Successors are produced with
|
|
15
|
+
:func:`dataclasses.replace` (the Python analogue of the TS spread).
|
|
16
|
+
- All branding lives in the single :class:`~induscode.workspace.Brand` record
|
|
17
|
+
and all on-disk paths in the single :class:`~induscode.workspace.Workspace`
|
|
18
|
+
record — both re-exported here so ``induscode.boot`` is a one-stop import
|
|
19
|
+
site, exactly as the TS barrel was.
|
|
20
|
+
- Where the rebuilt framework / app already owns a concept, the
|
|
21
|
+
resolved-resource graph is typed against the published type rather than
|
|
22
|
+
re-declared: framework ``Settings`` for the user-tunable config shape and
|
|
23
|
+
the conductor :class:`~induscode.conductor.ModelCatalog` for the model
|
|
24
|
+
registry (the TS anchors were ``indusagi/shell-app`` ``Settings`` and
|
|
25
|
+
``indusagi/ai`` ``ModelRegistry``; the port plan routes the catalog through
|
|
26
|
+
the conductor's normalized view).
|
|
27
|
+
|
|
28
|
+
Port notes
|
|
29
|
+
----------
|
|
30
|
+
- TS interfaces ``Stage`` / ``Runner`` were satisfied by frozen object
|
|
31
|
+
literals; the Python equivalents are frozen dataclasses whose ``apply`` /
|
|
32
|
+
``accepts`` / ``run`` members are plain callable *fields* (no ``self``
|
|
33
|
+
binding), so a stage/runner row is still data, not a class hierarchy.
|
|
34
|
+
- TS optional-absent fields become explicit ``None`` defaults; readonly
|
|
35
|
+
arrays become tuples. ``closables`` stays a *mutable list* shared across
|
|
36
|
+
``dataclasses.replace`` successors — accumulation is its purpose.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from typing import Literal, TypeAlias, Union
|
|
44
|
+
|
|
45
|
+
from indusagi.ai import ThinkingLevel
|
|
46
|
+
from indusagi.shell_app import Settings
|
|
47
|
+
|
|
48
|
+
from induscode.conductor import ModelCatalog
|
|
49
|
+
from induscode.workspace import Brand, Workspace
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"BootContext",
|
|
53
|
+
"Brand",
|
|
54
|
+
"Closable",
|
|
55
|
+
"CredentialGraph",
|
|
56
|
+
"Invocation",
|
|
57
|
+
"Runner",
|
|
58
|
+
"RunnerId",
|
|
59
|
+
"Stage",
|
|
60
|
+
"StartupResources",
|
|
61
|
+
"ThinkingLevel",
|
|
62
|
+
"Workspace",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Invocation
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
#: The three top-level execution modes the boot layer can dispatch to.
|
|
71
|
+
#:
|
|
72
|
+
#: - ``repl`` — interactive terminal session.
|
|
73
|
+
#: - ``oneshot`` — single non-interactive request to stdout (text or JSON).
|
|
74
|
+
#: - ``link`` — headless JSON-RPC link for a driving parent process.
|
|
75
|
+
RunnerId: TypeAlias = Literal["repl", "oneshot", "link"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class Invocation:
|
|
80
|
+
"""The parsed command line, reduced to what the boot layer needs to choose
|
|
81
|
+
and configure a :class:`Runner`.
|
|
82
|
+
|
|
83
|
+
This is the thin *routing* shape (the launch layer owns the rich parse —
|
|
84
|
+
:class:`induscode.launch.Invocation`); raw un-consumed tokens survive in
|
|
85
|
+
:attr:`rest` and loosely-typed switches in :attr:`flags` so nothing is
|
|
86
|
+
lost between the two. Field names are the mechanical snake_case renames of
|
|
87
|
+
the TS members (``modelId`` → ``model_id``, ``continueLatest`` →
|
|
88
|
+
``continue_latest``).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
#: Resolved execution mode.
|
|
92
|
+
mode: RunnerId
|
|
93
|
+
#: Parsed switches, keyed by canonical flag name (values intentionally loose).
|
|
94
|
+
flags: Mapping[str, object]
|
|
95
|
+
#: Positional / pass-through tokens not consumed as flags.
|
|
96
|
+
rest: tuple[str, ...]
|
|
97
|
+
#: First user message / request text, when supplied positionally.
|
|
98
|
+
prompt: str | None = None
|
|
99
|
+
#: Explicit model selector from the command line (``--model`` / ``-m``).
|
|
100
|
+
model_id: str | None = None
|
|
101
|
+
#: Working directory the run is scoped to; ``None`` means the process cwd.
|
|
102
|
+
cwd: str | None = None
|
|
103
|
+
#: Named credential account to authenticate with (``--account``).
|
|
104
|
+
account: str | None = None
|
|
105
|
+
#: Reasoning effort (``--thinking``): off/minimal/low/medium/high/xhigh.
|
|
106
|
+
thinking: str | None = None
|
|
107
|
+
#: Replacement system prompt (``--system``); path-or-literal.
|
|
108
|
+
system: str | None = None
|
|
109
|
+
#: Extra text appended after the system prompt (``--append-system``).
|
|
110
|
+
append_system: str | None = None
|
|
111
|
+
#: Allow-list of built-in tool names (``--tools``); ``None`` = full deck.
|
|
112
|
+
tools: tuple[str, ...] | None = None
|
|
113
|
+
#: Disable every built-in tool (``--no-tools``).
|
|
114
|
+
no_tools: bool = False
|
|
115
|
+
#: External MCP endpoint config paths to attach (``--mcp``).
|
|
116
|
+
mcp: tuple[str, ...] | None = None
|
|
117
|
+
#: Open the resume picker before the session starts (``--resume`` / ``-r``).
|
|
118
|
+
resume: bool | None = None
|
|
119
|
+
#: Auto-resume the most recent session in the cwd (``--continue`` / ``-c``).
|
|
120
|
+
continue_latest: bool | None = None
|
|
121
|
+
#: Print the model catalog and exit (``--list-models``).
|
|
122
|
+
list_models: bool | None = None
|
|
123
|
+
#: Optional substring filter for ``--list-models``.
|
|
124
|
+
list_models_filter: str | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Resolved startup resources
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
#: Placeholder for the resolved per-account credential graph (the TS
|
|
132
|
+
#: forward-declared indexable bag). The concrete multi-account vault lives in
|
|
133
|
+
#: :mod:`induscode.boot.auth_vault`; this stays a loose mapping so the
|
|
134
|
+
#: resource stage can attach it without coupling to the vault module.
|
|
135
|
+
CredentialGraph: TypeAlias = Mapping[str, object]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class StartupResources:
|
|
140
|
+
"""The resolved settings / auth / model-registry graph assembled during
|
|
141
|
+
startup.
|
|
142
|
+
|
|
143
|
+
``settings`` is typed against the framework's published ``Settings``
|
|
144
|
+
shape (degrading to an empty mapping when the framework piece cannot be
|
|
145
|
+
loaded — the *shape* is what later phases depend on); ``models`` is the
|
|
146
|
+
conductor's normalized :class:`~induscode.conductor.ModelCatalog` (the
|
|
147
|
+
port-plan replacement for the TS framework ``ModelRegistry``)."""
|
|
148
|
+
|
|
149
|
+
#: Merged user settings (framework-owned shape; empty mapping on degrade).
|
|
150
|
+
settings: Settings | Mapping[str, object]
|
|
151
|
+
#: Resolved credentials per account (app-owned placeholder bag).
|
|
152
|
+
auth: CredentialGraph
|
|
153
|
+
#: The resolved model catalog.
|
|
154
|
+
models: ModelCatalog
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Boot context & pipeline
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
#: One teardown callback drained on shutdown. May be sync or async; failures
|
|
162
|
+
#: are swallowed by the drain.
|
|
163
|
+
Closable: TypeAlias = Callable[[], Union[Awaitable[None], None]]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class BootContext:
|
|
168
|
+
"""The immutable value threaded through the :class:`Stage` pipeline.
|
|
169
|
+
|
|
170
|
+
Each stage receives a context and returns the next one; treat every field
|
|
171
|
+
as read-only and produce successors via :func:`dataclasses.replace` rather
|
|
172
|
+
than mutating. The resolved-resource graph is ``None`` until the resource
|
|
173
|
+
stage runs. :attr:`closables` accumulates teardown callbacks (open files,
|
|
174
|
+
servers, MCP clients) that the orchestrator drains in reverse on exit —
|
|
175
|
+
it is deliberately a shared mutable list across context successors.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
#: The process arguments the launch was invoked with (already sliced).
|
|
179
|
+
argv: tuple[str, ...]
|
|
180
|
+
#: Resolved on-disk layout.
|
|
181
|
+
workspace: Workspace
|
|
182
|
+
#: Resolved identity literals.
|
|
183
|
+
brand: Brand
|
|
184
|
+
#: Parsed command line.
|
|
185
|
+
invocation: Invocation
|
|
186
|
+
#: Resolved settings/auth/model graph; absent until the resource stage runs.
|
|
187
|
+
resources: StartupResources | None = None
|
|
188
|
+
#: Teardown callbacks to drain on shutdown (latest-registered first).
|
|
189
|
+
closables: list[Closable] = field(default_factory=list)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True, slots=True)
|
|
193
|
+
class Stage:
|
|
194
|
+
"""One step of the launch pipeline: a named, pure-ish transform over a
|
|
195
|
+
:class:`BootContext`.
|
|
196
|
+
|
|
197
|
+
``apply`` may return its result synchronously or as an awaitable. It must
|
|
198
|
+
not mutate its input; it returns the next context. ``name`` is used for
|
|
199
|
+
tracing and error attribution. (TS interface → frozen dataclass row whose
|
|
200
|
+
``apply`` is a plain callable field.)
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
#: Stable identifier for tracing and error messages.
|
|
204
|
+
name: str
|
|
205
|
+
#: Transform the context, yielding its successor.
|
|
206
|
+
apply: Callable[[BootContext], Union[BootContext, Awaitable[BootContext]]]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True, slots=True)
|
|
210
|
+
class Runner:
|
|
211
|
+
"""A terminal execution strategy for one :data:`RunnerId` mode.
|
|
212
|
+
|
|
213
|
+
The runner registry asks each runner whether it ``accepts`` a parsed
|
|
214
|
+
:class:`Invocation`; the first match runs. ``run`` drives the selected
|
|
215
|
+
mode to completion and resolves to the process exit code.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
#: The mode this runner serves.
|
|
219
|
+
id: RunnerId
|
|
220
|
+
#: Whether this runner handles the given invocation.
|
|
221
|
+
accepts: Callable[[Invocation], bool]
|
|
222
|
+
#: Execute the mode; resolves to the process exit code.
|
|
223
|
+
run: Callable[[BootContext], Awaitable[int]]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Invocation reader for the boot layer — the adapter that drives the full
|
|
2
|
+
declarative launch flag grammar and projects its rich result down onto the
|
|
3
|
+
thin boot :class:`~.contract.Invocation` the runner pipeline routes on.
|
|
4
|
+
|
|
5
|
+
Port of TS ``src/boot/invocation.ts``. The parsing itself is not owned here:
|
|
6
|
+
:func:`induscode.launch.read_invocation` walks the single declarative flag
|
|
7
|
+
table and produces the full launch :class:`~induscode.launch.Invocation`;
|
|
8
|
+
this module's job is purely the *projection* — map the launch
|
|
9
|
+
:data:`~induscode.launch.OutputMode` onto the boot
|
|
10
|
+
:data:`~.contract.RunnerId`, rename the resolved fields the boot layer reads
|
|
11
|
+
(``model`` → ``model_id``, ``positionals`` → ``rest``), and carry the loose
|
|
12
|
+
flag bag through so the meta checks (``--help`` / ``--version``) and the
|
|
13
|
+
oneshot output-shape probe (``json``) keep reading the same canonical keys.
|
|
14
|
+
|
|
15
|
+
- mode: ``text`` → ``repl``, ``json`` → ``oneshot``, ``rpc`` → ``link``.
|
|
16
|
+
|
|
17
|
+
The parse is total and never raises: the launch reader tolerates unknown
|
|
18
|
+
``--flags`` as switches in the flag bag rather than rejecting them, so
|
|
19
|
+
nothing is silently dropped before a later phase can reinterpret it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
|
|
26
|
+
from induscode.launch import Invocation as LaunchInvocation
|
|
27
|
+
from induscode.launch import OutputMode, read_invocation
|
|
28
|
+
|
|
29
|
+
from .contract import Invocation, RunnerId
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"to_runner_id",
|
|
33
|
+
"tokenize_invocation",
|
|
34
|
+
"wants_help",
|
|
35
|
+
"wants_version",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_runner_id(mode: OutputMode) -> RunnerId:
|
|
40
|
+
"""Map a launch :data:`OutputMode` onto the boot :data:`RunnerId` the
|
|
41
|
+
runner registry dispatches on. The two vocabularies are deliberately
|
|
42
|
+
one-to-one:
|
|
43
|
+
|
|
44
|
+
- ``text`` — the interactive terminal session → ``repl``.
|
|
45
|
+
- ``json`` — a single non-interactive request to stdout → ``oneshot``.
|
|
46
|
+
- ``rpc`` — the headless line protocol for a driving parent → ``link``.
|
|
47
|
+
"""
|
|
48
|
+
if mode == "rpc":
|
|
49
|
+
return "link"
|
|
50
|
+
if mode == "json":
|
|
51
|
+
return "oneshot"
|
|
52
|
+
return "repl"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _project_invocation(launch: LaunchInvocation) -> Invocation:
|
|
56
|
+
"""Project a fully-parsed launch invocation down onto the thin boot
|
|
57
|
+
:class:`~.contract.Invocation`.
|
|
58
|
+
|
|
59
|
+
The boot layer only needs the routing mode, the prompt, the explicit
|
|
60
|
+
model and cwd, the leftover positionals, and the loose flag bag; the
|
|
61
|
+
richer launch fields (attachments, typed tool roster, …) stay available
|
|
62
|
+
on the launch result for later phases.
|
|
63
|
+
"""
|
|
64
|
+
# `--list-models` is a string flag (its value is an optional substring
|
|
65
|
+
# filter), so "present" means set to anything other than False; a
|
|
66
|
+
# non-empty string value becomes the filter.
|
|
67
|
+
list_models_raw = launch.flags.get("list-models")
|
|
68
|
+
list_models = list_models_raw is not None and list_models_raw is not False
|
|
69
|
+
list_models_filter = (
|
|
70
|
+
list_models_raw
|
|
71
|
+
if isinstance(list_models_raw, str) and len(list_models_raw) > 0
|
|
72
|
+
else None
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return Invocation(
|
|
76
|
+
mode=to_runner_id(launch.mode),
|
|
77
|
+
prompt=launch.prompt,
|
|
78
|
+
model_id=launch.model,
|
|
79
|
+
cwd=launch.cwd,
|
|
80
|
+
account=launch.account,
|
|
81
|
+
thinking=launch.thinking,
|
|
82
|
+
system=launch.system,
|
|
83
|
+
append_system=launch.append_system,
|
|
84
|
+
tools=launch.tools,
|
|
85
|
+
no_tools=bool(launch.no_tools),
|
|
86
|
+
mcp=launch.mcp if len(launch.mcp) > 0 else None,
|
|
87
|
+
resume=True if launch.flags.get("resume") is True else None,
|
|
88
|
+
continue_latest=True if launch.flags.get("continue") is True else None,
|
|
89
|
+
list_models=True if list_models else None,
|
|
90
|
+
list_models_filter=list_models_filter,
|
|
91
|
+
flags=launch.flags,
|
|
92
|
+
rest=tuple(launch.positionals),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def tokenize_invocation(argv: Sequence[str]) -> Invocation:
|
|
97
|
+
"""Read a sliced ``argv`` into the boot :class:`~.contract.Invocation`.
|
|
98
|
+
|
|
99
|
+
Delegates the actual grammar to the launch
|
|
100
|
+
:func:`~induscode.launch.read_invocation` (the one declarative flag
|
|
101
|
+
table), then projects its result onto the boot shape — help, parsing, and
|
|
102
|
+
routing share one parser and can never drift.
|
|
103
|
+
|
|
104
|
+
:param argv: the already-sliced argument vector (no interpreter / script
|
|
105
|
+
path)
|
|
106
|
+
"""
|
|
107
|
+
return _project_invocation(read_invocation(argv))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def wants_help(inv: Invocation) -> bool:
|
|
111
|
+
"""Whether the parsed invocation is asking for the help banner."""
|
|
112
|
+
return inv.flags.get("help") is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def wants_version(inv: Invocation) -> bool:
|
|
116
|
+
"""Whether the parsed invocation is asking for the version string."""
|
|
117
|
+
return inv.flags.get("version") is True
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Runners subsystem — public barrel (port of TS ``src/boot/runners``).
|
|
2
|
+
|
|
3
|
+
Surfaces the runner registry and the individual runners. Boot consumers
|
|
4
|
+
dispatch through :func:`select_runner` / :data:`RUNNERS`; the named runners
|
|
5
|
+
are exported for tests and for explicit wiring, along with the session
|
|
6
|
+
assembly helpers (:func:`build_session_conductor`, :func:`session_scope_dir`)
|
|
7
|
+
and the repl console-mount seam M5 plugs into.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .link_runner import link_runner
|
|
13
|
+
from .oneshot_runner import oneshot_runner
|
|
14
|
+
from .registry import RUNNERS, select_runner
|
|
15
|
+
from .repl_runner import ConsoleMount, ReplServices, repl_runner, set_console_mount
|
|
16
|
+
from .session import (
|
|
17
|
+
build_key_resolver,
|
|
18
|
+
build_session_conductor,
|
|
19
|
+
condense_transcript,
|
|
20
|
+
oneshot_prompts,
|
|
21
|
+
prime_provider_env,
|
|
22
|
+
resolve_model_id,
|
|
23
|
+
session_scope_dir,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ConsoleMount",
|
|
28
|
+
"RUNNERS",
|
|
29
|
+
"ReplServices",
|
|
30
|
+
"build_key_resolver",
|
|
31
|
+
"build_session_conductor",
|
|
32
|
+
"condense_transcript",
|
|
33
|
+
"link_runner",
|
|
34
|
+
"oneshot_prompts",
|
|
35
|
+
"oneshot_runner",
|
|
36
|
+
"prime_provider_env",
|
|
37
|
+
"repl_runner",
|
|
38
|
+
"resolve_model_id",
|
|
39
|
+
"select_runner",
|
|
40
|
+
"session_scope_dir",
|
|
41
|
+
"set_console_mount",
|
|
42
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Runner: ``link`` — headless JSON-RPC link for a driving parent process.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/boot/runners/link-runner.ts``. Drives the link channel: it
|
|
4
|
+
assembles a :class:`~induscode.conductor.SessionConductor` for the invocation
|
|
5
|
+
and serves the declarative :data:`~induscode.channels.SESSION_OPS` registry
|
|
6
|
+
over the process stdio pair via
|
|
7
|
+
:func:`~induscode.channels.create_link_server`. Every framed request is
|
|
8
|
+
dispatched through the registry (data-driven, not a command switch) and the
|
|
9
|
+
reply is framed back; the runner resolves the success exit code once the
|
|
10
|
+
inbound stream ends.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
from typing import Final
|
|
19
|
+
|
|
20
|
+
from induscode.channels import SESSION_OPS, WritableLine, create_link_server
|
|
21
|
+
from induscode.channels.link import LinkServerIo
|
|
22
|
+
|
|
23
|
+
from ..contract import BootContext, Invocation, Runner
|
|
24
|
+
from .session import build_session_conductor
|
|
25
|
+
|
|
26
|
+
__all__ = ["link_runner"]
|
|
27
|
+
|
|
28
|
+
#: Exit code returned once the link stream ends cleanly.
|
|
29
|
+
_EXIT_OK: Final[int] = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Stdout:
|
|
33
|
+
"""A :class:`~induscode.channels.WritableLine` backed by the process
|
|
34
|
+
standard output stream (flushed per frame — a driving parent reads the
|
|
35
|
+
pipe line by line)."""
|
|
36
|
+
|
|
37
|
+
def write(self, chunk: str) -> object:
|
|
38
|
+
sys.stdout.write(chunk)
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: The shared stdout sink.
|
|
44
|
+
STDOUT: Final[WritableLine] = _Stdout()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _stdin_chunks() -> AsyncIterator[bytes]:
|
|
48
|
+
"""The process stdin as an async chunk stream (the
|
|
49
|
+
:data:`~induscode.channels.ReadableChunks` shape). Blocking reads are
|
|
50
|
+
pushed onto the default executor so the event loop — and with it the
|
|
51
|
+
concurrently-dispatched request handlers — keeps running between
|
|
52
|
+
frames."""
|
|
53
|
+
loop = asyncio.get_running_loop()
|
|
54
|
+
stdin = sys.stdin.buffer
|
|
55
|
+
while True:
|
|
56
|
+
chunk = await loop.run_in_executor(None, stdin.read1, 65536)
|
|
57
|
+
if not chunk:
|
|
58
|
+
return
|
|
59
|
+
yield chunk
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _accepts(inv: Invocation) -> bool:
|
|
63
|
+
return inv.mode == "link"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _run(ctx: BootContext) -> int:
|
|
67
|
+
"""Assemble the conductor, start a link server reading framed requests
|
|
68
|
+
from stdin and writing framed replies to stdout, and resolve the success
|
|
69
|
+
exit code when the inbound stream is exhausted."""
|
|
70
|
+
conductor = await build_session_conductor(ctx)
|
|
71
|
+
server = create_link_server(
|
|
72
|
+
SESSION_OPS,
|
|
73
|
+
conductor,
|
|
74
|
+
LinkServerIo(in_=_stdin_chunks(), out=STDOUT),
|
|
75
|
+
)
|
|
76
|
+
await server.done
|
|
77
|
+
return _EXIT_OK
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
#: The headless-link runner row: accepts invocations whose resolved mode is
|
|
81
|
+
#: ``link``.
|
|
82
|
+
link_runner: Final[Runner] = Runner(id="link", accepts=_accepts, run=_run)
|