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,170 @@
|
|
|
1
|
+
"""Window-budget — the condenser factory (the conductor-consumable seam;
|
|
2
|
+
port of TS ``src/window-budget/condenser.ts``).
|
|
3
|
+
|
|
4
|
+
:func:`create_condenser` ties the three pure pieces together into a single
|
|
5
|
+
function the conductor can drop into its auto-compaction hook:
|
|
6
|
+
|
|
7
|
+
1. **Gate** — :func:`~.budget.is_over_budget` asks "is this transcript
|
|
8
|
+
large enough to condense?" against the bound model's window and the
|
|
9
|
+
:class:`~.contract.BudgetPolicy`.
|
|
10
|
+
2. **Slice** — :func:`~.budget.plan_slice` splits the transcript into a
|
|
11
|
+
``dropped`` head (older turns to fold away) and a verbatim ``kept`` tail,
|
|
12
|
+
never cutting between a ``toolCall`` and its ``toolResult``.
|
|
13
|
+
3. **Condense** — :func:`~.summarize.summarize` turns the dropped head into
|
|
14
|
+
one synthetic digest message (scope ``"session"``), via the injectable
|
|
15
|
+
model completer.
|
|
16
|
+
|
|
17
|
+
The returned value is a :data:`~.contract.Condenser` — structurally the
|
|
18
|
+
conductor ``CondenseFn``
|
|
19
|
+
(``(messages) -> list[AgentMessage] | Awaitable[list[AgentMessage]]``):
|
|
20
|
+
|
|
21
|
+
- When over budget it returns ``[summary_message, *kept]`` (a strictly
|
|
22
|
+
smaller transcript: one digest in place of the dropped prefix, plus the
|
|
23
|
+
recent tail).
|
|
24
|
+
- Otherwise it returns the input list unchanged — the SAME list object (the
|
|
25
|
+
no-op identity the conductor expects when nothing needs doing; tests pin
|
|
26
|
+
``is``-identity on this path).
|
|
27
|
+
|
|
28
|
+
It is **network-free-testable**: with no ``model`` bound the gate cannot
|
|
29
|
+
measure a window, so the condenser is an identity transform; inject a
|
|
30
|
+
:class:`~.contract.CompleteFn` stub (and a ``model``) to drive the summary
|
|
31
|
+
path with no real API call.
|
|
32
|
+
|
|
33
|
+
Branch-archival is *not* a second engine — :func:`condense_scope` forwards to
|
|
34
|
+
the shared :func:`~.summarize.summarize` core pinned to scope ``"branch"``.
|
|
35
|
+
:func:`condense` is the matching session-scope entrypoint. Both are
|
|
36
|
+
re-exported here so callers can summarize a slice directly without building a
|
|
37
|
+
full ``Condenser``.
|
|
38
|
+
|
|
39
|
+
TWO COMPACTION ENGINES — KEEP THEM DISTINCT (analysis 05 §1 / risk-3)
|
|
40
|
+
---------------------------------------------------------------------
|
|
41
|
+
This module is the app's **conductor ``CondenseFn``** seam. The framework
|
|
42
|
+
ships its own compactor, :mod:`indusagi.runtime.memory`
|
|
43
|
+
(``should_compact`` / ``find_cut_point`` / ``summarize`` / ``compact``), but
|
|
44
|
+
it is a DIFFERENT engine and is never wired into the conductor:
|
|
45
|
+
|
|
46
|
+
============ ============================================= ==========================================
|
|
47
|
+
window_budget (this module) indusagi.runtime.memory (framework)
|
|
48
|
+
============ ============================================= ==========================================
|
|
49
|
+
message type ``indusagi.ai`` ``AgentMessage`` (+ agent ``llmgateway.contract.Turn``
|
|
50
|
+
custom messages)
|
|
51
|
+
keep_recent **tokens** (default 6000) **turns** (default 8)
|
|
52
|
+
trigger ratio **0.75** of the window ratio **0.8**
|
|
53
|
+
completer ``indusagi.ai.complete_simple`` (injectable) ``ModelInvoker``
|
|
54
|
+
digest head ``[session digest …]`` / ``[branch digest …]`` ``[condensed earlier context]``
|
|
55
|
+
============ ============================================= ==========================================
|
|
56
|
+
|
|
57
|
+
Do not substitute one for the other: the policies, prompts, and digest
|
|
58
|
+
headers differ, and tests pin this module's headers.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
from dataclasses import replace
|
|
64
|
+
|
|
65
|
+
from .budget import is_over_budget, plan_slice
|
|
66
|
+
from .contract import (
|
|
67
|
+
AgentMessage,
|
|
68
|
+
BudgetPolicy,
|
|
69
|
+
CompleteFn,
|
|
70
|
+
Condenser,
|
|
71
|
+
CondenserDeps,
|
|
72
|
+
Model,
|
|
73
|
+
Summary,
|
|
74
|
+
)
|
|
75
|
+
from .summarize import SummarizeDeps, condense_scope, summarize
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"DEFAULT_POLICY",
|
|
79
|
+
"condense",
|
|
80
|
+
"condense_scope",
|
|
81
|
+
"create_condenser",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Default policy (config-sourced thresholds)
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
#: The fallback :class:`~.contract.BudgetPolicy` when the caller supplies
|
|
90
|
+
#: none. These are ordinary configuration defaults — a window-relative ratio
|
|
91
|
+
#: plus a token tail — computed from the model's own context window rather
|
|
92
|
+
#: than fixed magic numbers.
|
|
93
|
+
#:
|
|
94
|
+
#: - ``trigger_ratio`` 0.75 — condense once ~three-quarters of the window is used.
|
|
95
|
+
#: - ``keep_recent`` 6000 — keep roughly the last 6k tokens of turns verbatim.
|
|
96
|
+
#: - ``reserve_tokens`` 2048 — carve a little headroom off the window first.
|
|
97
|
+
DEFAULT_POLICY = BudgetPolicy(
|
|
98
|
+
trigger_ratio=0.75,
|
|
99
|
+
keep_recent=6000,
|
|
100
|
+
reserve_tokens=2048,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Factory
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_condenser(deps: CondenserDeps | None = None) -> Condenser:
|
|
110
|
+
"""Build a :data:`~.contract.Condenser` from a
|
|
111
|
+
:class:`~.contract.CondenserDeps` bundle. The result plugs straight into
|
|
112
|
+
the conductor's ``CondenseFn`` seam.
|
|
113
|
+
|
|
114
|
+
Behavior of the returned condenser, given a transcript ``messages``:
|
|
115
|
+
|
|
116
|
+
- If no ``model`` is bound (so the window is unknown), or the transcript
|
|
117
|
+
is **not** over budget, return ``messages`` **unchanged** (the same
|
|
118
|
+
list object).
|
|
119
|
+
- Otherwise ``plan_slice`` the transcript; if there is nothing to drop
|
|
120
|
+
(``cut == 0``) return it unchanged; else ``summarize`` the dropped head
|
|
121
|
+
(scope ``"session"``) and return ``[summary.message, *plan.kept]``.
|
|
122
|
+
|
|
123
|
+
All dependencies are optional, so ``create_condenser()`` yields a
|
|
124
|
+
working, network-free condenser (a no-op until a model and policy are
|
|
125
|
+
supplied).
|
|
126
|
+
"""
|
|
127
|
+
if deps is None:
|
|
128
|
+
deps = CondenserDeps()
|
|
129
|
+
policy: BudgetPolicy = deps.policy if deps.policy is not None else DEFAULT_POLICY
|
|
130
|
+
model: Model | None = deps.model
|
|
131
|
+
complete: CompleteFn | None = deps.complete
|
|
132
|
+
|
|
133
|
+
async def condenser(messages: list[AgentMessage]) -> list[AgentMessage]:
|
|
134
|
+
# No model → no window to measure against → nothing to do (identity).
|
|
135
|
+
if model is None:
|
|
136
|
+
return messages
|
|
137
|
+
|
|
138
|
+
# Under budget → leave the transcript exactly as-is (conductor no-op).
|
|
139
|
+
if not is_over_budget(messages, model, policy):
|
|
140
|
+
return messages
|
|
141
|
+
|
|
142
|
+
# Decide where to cut (forward prefix-sum + binary search, tool-pair safe).
|
|
143
|
+
plan = plan_slice(messages, policy)
|
|
144
|
+
if plan.cut == 0 or not plan.dropped:
|
|
145
|
+
# Nothing condensable (everything is within the recent tail) → identity.
|
|
146
|
+
return messages
|
|
147
|
+
|
|
148
|
+
# Fold the dropped head into one synthetic digest, then splice it
|
|
149
|
+
# ahead of the verbatim recent tail.
|
|
150
|
+
summary = await condense(plan.dropped, SummarizeDeps(complete=complete, model=model))
|
|
151
|
+
return [summary.message, *plan.kept]
|
|
152
|
+
|
|
153
|
+
return condenser
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Direct summarization entrypoints (session + branch)
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def condense(
|
|
162
|
+
messages: list[AgentMessage],
|
|
163
|
+
deps: SummarizeDeps | None = None,
|
|
164
|
+
) -> Summary:
|
|
165
|
+
"""Session-scope summarization: fold a slice of dropped messages into one
|
|
166
|
+
digest for the active session. A thin wrapper over the shared
|
|
167
|
+
:func:`~.summarize.summarize` core pinned to scope ``"session"`` (the
|
|
168
|
+
default), so callers don't pass the flag."""
|
|
169
|
+
base = deps if deps is not None else SummarizeDeps()
|
|
170
|
+
return await summarize(messages, replace(base, scope="session"))
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Window-budget contract — the FROZEN type surface of the context-window
|
|
2
|
+
budgeting subsystem (port of TS ``src/window-budget/contract.ts``).
|
|
3
|
+
|
|
4
|
+
A long-running coding session accrues messages — user turns, assistant turns,
|
|
5
|
+
tool calls and their results — until the transcript approaches the model's
|
|
6
|
+
context window. This subsystem keeps the session usable by *condensing* older
|
|
7
|
+
history into a single structured summary message while keeping a verbatim
|
|
8
|
+
tail of the most-recent turns. It answers three questions:
|
|
9
|
+
|
|
10
|
+
1. **Budget** — *when* are we over budget? A :class:`BudgetPolicy` carries
|
|
11
|
+
config-sourced thresholds (no magic literals);
|
|
12
|
+
:func:`~induscode.window_budget.budget.gate.is_over_budget` decides from a
|
|
13
|
+
real :class:`TokenEstimate`.
|
|
14
|
+
2. **Slice** — *where* do we cut?
|
|
15
|
+
:func:`~induscode.window_budget.budget.slice.plan_slice` computes a
|
|
16
|
+
:class:`CondensePlan` via a forward prefix-sum + binary search over a
|
|
17
|
+
cumulative-token array, never splitting a tool_call from its tool_result.
|
|
18
|
+
3. **Condense** — *how* do we compress the dropped portion? A
|
|
19
|
+
:data:`Condenser` flattens the dropped messages, asks an injectable model
|
|
20
|
+
completer for a structured :class:`Summary`, and returns the rebuilt
|
|
21
|
+
transcript (summary + kept tail).
|
|
22
|
+
|
|
23
|
+
This module declares *only* shapes plus the function-type seams — no behavior,
|
|
24
|
+
no I/O, no prompt strings. Every later window-budget module (the meter, the
|
|
25
|
+
slice planner, the condenser, the branch archivist, the prompt templates, and
|
|
26
|
+
the public barrel) is written against the names declared here.
|
|
27
|
+
|
|
28
|
+
Design stance:
|
|
29
|
+
|
|
30
|
+
- Thresholds are **configuration, not constants**. :class:`BudgetPolicy`
|
|
31
|
+
fields are supplied by the caller; this module bakes in no fixed token
|
|
32
|
+
fingerprints. Defaults, if any, live in the implementation and are computed
|
|
33
|
+
relative to the model's own context window.
|
|
34
|
+
- Branch-summarization is **not a separate engine**. It collapses into the
|
|
35
|
+
condenser behind a :data:`CondenseScope` flag (``"session" | "branch"``),
|
|
36
|
+
so there is one prompt-assembly path, one token estimator, one slicer.
|
|
37
|
+
- The model completer is **injectable** (:class:`CompleteFn`, default the
|
|
38
|
+
framework's :func:`indusagi.ai.complete_simple`) so the condenser runs in
|
|
39
|
+
tests with no network.
|
|
40
|
+
- The produced condenser is **conductor-consumable**: a :data:`Condenser` is
|
|
41
|
+
structurally the conductor ``CondenseFn``
|
|
42
|
+
(``(messages) -> list[AgentMessage] | Awaitable[list[AgentMessage]]``), so a
|
|
43
|
+
:class:`CondenserDeps`-built condenser plugs straight into the conductor's
|
|
44
|
+
auto-compaction seam.
|
|
45
|
+
|
|
46
|
+
Port notes (Python framework deltas vs the TS source)
|
|
47
|
+
-----------------------------------------------------
|
|
48
|
+
- **``AgentMessage`` relocated AND narrower.** In TS it came from
|
|
49
|
+
``indusagi/agent`` and the declaration-merged union already covered the
|
|
50
|
+
agent's custom session messages. In the Python framework the alias lives at
|
|
51
|
+
:data:`indusagi.ai.AgentMessage` and equals the bare LLM ``Message`` union
|
|
52
|
+
(``UserMessage | AssistantMessage | ToolResultMessage``) only. The agent's
|
|
53
|
+
custom message kinds — :class:`indusagi.agent.BashExecutionMessage`,
|
|
54
|
+
:class:`indusagi.agent.CustomMessage`,
|
|
55
|
+
:class:`indusagi.agent.BranchSummaryMessage`,
|
|
56
|
+
:class:`indusagi.agent.CompactionSummaryMessage` — are separate dataclasses
|
|
57
|
+
in :mod:`indusagi.agent`. This contract therefore **explicitly unions them**
|
|
58
|
+
into the app-level :data:`AgentMessage` alias, and every role-probing
|
|
59
|
+
consumer (the estimator, the slicer, the flattener) handles those roles via
|
|
60
|
+
``getattr``/``isinstance`` duck probing, never dict access — messages are
|
|
61
|
+
frozen dataclasses here, not JSON records.
|
|
62
|
+
- ``Model`` drops the TS ``<TApi>`` generic — :class:`indusagi.ai.Model`
|
|
63
|
+
carries ``api`` as a plain string tag.
|
|
64
|
+
- The Web ``AbortSignal`` becomes the framework's ``CancelToken`` (forwarded
|
|
65
|
+
opaquely into ``SimpleStreamOptions.signal``; not re-typed here because the
|
|
66
|
+
framework keeps it internal).
|
|
67
|
+
|
|
68
|
+
Framework anchors (all from the ``indusagi`` package — the sibling rebuilt
|
|
69
|
+
framework this app targets):
|
|
70
|
+
|
|
71
|
+
- ``AgentMessage`` (base union), ``Model``, ``Usage``, ``complete_simple``
|
|
72
|
+
(and its ``Api`` / ``AssistantMessage`` / ``Context`` /
|
|
73
|
+
``SimpleStreamOptions`` / ``StreamLogger`` shapes) ← ``indusagi.ai``
|
|
74
|
+
- ``BashExecutionMessage`` / ``CustomMessage`` / ``BranchSummaryMessage`` /
|
|
75
|
+
``CompactionSummaryMessage`` ← ``indusagi.agent``
|
|
76
|
+
|
|
77
|
+
This module never re-declares these; it composes them.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
from collections.abc import Awaitable, Callable
|
|
83
|
+
from dataclasses import dataclass
|
|
84
|
+
from typing import Literal, Protocol, TypeAlias
|
|
85
|
+
|
|
86
|
+
from indusagi.agent import (
|
|
87
|
+
BashExecutionMessage,
|
|
88
|
+
BranchSummaryMessage,
|
|
89
|
+
CompactionSummaryMessage,
|
|
90
|
+
CustomMessage,
|
|
91
|
+
)
|
|
92
|
+
from indusagi.ai import (
|
|
93
|
+
AgentMessage as AiAgentMessage,
|
|
94
|
+
)
|
|
95
|
+
from indusagi.ai import (
|
|
96
|
+
Api,
|
|
97
|
+
AssistantMessage,
|
|
98
|
+
Context,
|
|
99
|
+
Model,
|
|
100
|
+
SimpleStreamOptions,
|
|
101
|
+
StreamLogger,
|
|
102
|
+
Usage,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"AgentMessage",
|
|
107
|
+
"Api",
|
|
108
|
+
"BudgetPolicy",
|
|
109
|
+
"CompleteFn",
|
|
110
|
+
"CondensePlan",
|
|
111
|
+
"CondenseScope",
|
|
112
|
+
"Condenser",
|
|
113
|
+
"CondenserDeps",
|
|
114
|
+
"Model",
|
|
115
|
+
"Summary",
|
|
116
|
+
"TokenEstimate",
|
|
117
|
+
"Usage",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Message vocabulary (re-exported framework vocabulary, widened)
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
#: The transcript message union this subsystem operates on. The framework's
|
|
126
|
+
#: :data:`indusagi.ai.AgentMessage` covers only the LLM ``Message`` trio, so —
|
|
127
|
+
#: unlike the TS source, where declaration merging widened it implicitly —
|
|
128
|
+
#: the agent's four custom session-message dataclasses are unioned in
|
|
129
|
+
#: EXPLICITLY here (analysis 05 risk-2). A silent omission would mis-estimate
|
|
130
|
+
#: tokens and skip those turns in digests.
|
|
131
|
+
AgentMessage: TypeAlias = (
|
|
132
|
+
AiAgentMessage
|
|
133
|
+
| BashExecutionMessage
|
|
134
|
+
| CustomMessage
|
|
135
|
+
| BranchSummaryMessage
|
|
136
|
+
| CompactionSummaryMessage
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Policy
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
146
|
+
class BudgetPolicy:
|
|
147
|
+
"""The config-sourced thresholds that govern when and how the transcript
|
|
148
|
+
is condensed. Every field is a *number the caller supplies* — there are no
|
|
149
|
+
magic token literals baked into this contract. An implementation may
|
|
150
|
+
provide defaults, but they are configuration values, not protected
|
|
151
|
+
constants, and deliberately differ from the legacy fingerprints.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
# Fraction of the model's `contextWindow` at which condensing is
|
|
155
|
+
# triggered, in (0, 1]. The meter is "over budget" once the estimated
|
|
156
|
+
# context tokens cross `contextWindow * trigger_ratio`. Expressing the
|
|
157
|
+
# trigger as a ratio (rather than an absolute reserve) keeps it
|
|
158
|
+
# window-relative and free of baked-in token counts.
|
|
159
|
+
trigger_ratio: float
|
|
160
|
+
|
|
161
|
+
# How much of the recent transcript to keep verbatim, measured in
|
|
162
|
+
# estimated tokens. The slice planner preserves a tail of at least this
|
|
163
|
+
# many tokens (snapped to a legal boundary) and condenses everything
|
|
164
|
+
# before it.
|
|
165
|
+
keep_recent: int
|
|
166
|
+
|
|
167
|
+
# Optional token headroom to subtract from the window before the trigger
|
|
168
|
+
# is evaluated — a safety margin so a condense fires *before* the very
|
|
169
|
+
# next turn would overflow. When omitted, only `trigger_ratio` governs
|
|
170
|
+
# the trigger.
|
|
171
|
+
reserve_tokens: int | None = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Token measurement
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
180
|
+
class TokenEstimate:
|
|
181
|
+
"""The outcome of measuring a transcript against a model's window.
|
|
182
|
+
|
|
183
|
+
Anchors on the provider's authoritative ``usage`` count when available
|
|
184
|
+
(``anchored=True``) and falls back to a rough char/role estimate
|
|
185
|
+
otherwise; either way ``context_tokens`` is the figure ``is_over_budget``
|
|
186
|
+
compares to ``context_window``.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
# Best estimate of tokens currently occupying the context window.
|
|
190
|
+
context_tokens: int
|
|
191
|
+
|
|
192
|
+
# The model's total context window, in tokens (`Model.contextWindow`).
|
|
193
|
+
context_window: int
|
|
194
|
+
|
|
195
|
+
# True when `context_tokens` is anchored on a real provider `usage`
|
|
196
|
+
# figure (only trailing, post-usage messages are estimated); False when
|
|
197
|
+
# the whole figure is a heuristic estimate.
|
|
198
|
+
anchored: bool
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Slice plan
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
207
|
+
class CondensePlan:
|
|
208
|
+
"""The pre-computed result of ``plan_slice``: the chosen cut index and the
|
|
209
|
+
resulting partition of the transcript.
|
|
210
|
+
|
|
211
|
+
The cut is found by a *forward* prefix-sum + binary search: build a
|
|
212
|
+
cumulative estimated-token array over the messages, binary-search for the
|
|
213
|
+
boundary index nearest ``total - keep_recent``, then nudge to the closest
|
|
214
|
+
*legal* boundary — never landing between a ``toolCall`` and its
|
|
215
|
+
``toolResult``.
|
|
216
|
+
|
|
217
|
+
Invariants:
|
|
218
|
+
|
|
219
|
+
- ``len(kept) + len(dropped) == len(messages)``
|
|
220
|
+
- ``dropped`` is the prefix ``messages[0:cut]`` (the older slice to condense)
|
|
221
|
+
- ``kept`` is the suffix ``messages[cut:]`` (the verbatim recent tail)
|
|
222
|
+
- ``cut`` is ``0`` when nothing is condensable (everything kept).
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
# Index of the legal cut boundary: dropped = messages[:cut].
|
|
226
|
+
cut: int
|
|
227
|
+
|
|
228
|
+
# The verbatim recent tail preserved as-is (messages[cut:]).
|
|
229
|
+
kept: list[AgentMessage]
|
|
230
|
+
|
|
231
|
+
# The older slice to be folded into a `Summary` (messages[:cut]).
|
|
232
|
+
dropped: list[AgentMessage]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Condense scope
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
#: Which kind of condensing run this is — the flag that collapses
|
|
240
|
+
#: branch-summarization into the single condenser path:
|
|
241
|
+
#:
|
|
242
|
+
#: - ``"session"`` — condense the *active* transcript in place (the trigger /
|
|
243
|
+
#: overflow path); keeps a recent tail, summarizes the head.
|
|
244
|
+
#: - ``"branch"`` — archive an *abandoned* branch into one summary message so
|
|
245
|
+
#: its context isn't lost on tree navigation; no verbatim tail is retained.
|
|
246
|
+
CondenseScope: TypeAlias = Literal["session", "branch"]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Summary
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
255
|
+
class Summary:
|
|
256
|
+
"""The structured result of summarizing a slice of dropped messages: a
|
|
257
|
+
single synthetic :data:`AgentMessage` that stands in for the messages it
|
|
258
|
+
covers.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# The synthetic summary message to splice into the rebuilt transcript.
|
|
262
|
+
message: AgentMessage
|
|
263
|
+
|
|
264
|
+
# How many source messages this summary replaces.
|
|
265
|
+
covered_count: int
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# Model completer seam (injectable, default = framework complete_simple)
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class CompleteFn(Protocol):
|
|
274
|
+
"""The injectable model-completion function. Structurally identical to
|
|
275
|
+
the framework's :func:`indusagi.ai.complete_simple`
|
|
276
|
+
(``(model, context, options=None, logger=None) -> AssistantMessage``), so
|
|
277
|
+
the real ``complete_simple`` is the drop-in default and a test can pass a
|
|
278
|
+
no-network stub of the same shape. The condenser uses this single call to
|
|
279
|
+
turn flattened transcript text into a :class:`Summary`.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __call__(
|
|
283
|
+
self,
|
|
284
|
+
model: Model,
|
|
285
|
+
context: Context,
|
|
286
|
+
options: SimpleStreamOptions | None = None,
|
|
287
|
+
logger: StreamLogger | None = None,
|
|
288
|
+
) -> Awaitable[AssistantMessage]: ...
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Condenser factory seam
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
#: A condenser: takes the active branch's messages and returns the rebuilt
|
|
296
|
+
#: transcript (condensed head + verbatim tail, or the input unchanged when
|
|
297
|
+
#: there is nothing to do).
|
|
298
|
+
#:
|
|
299
|
+
#: This type is **structurally compatible with the conductor's
|
|
300
|
+
#: ``CondenseFn``** (``(messages) -> list[AgentMessage] |
|
|
301
|
+
#: Awaitable[list[AgentMessage]]``), so the result of the condenser factory
|
|
302
|
+
#: plugs directly into the conductor's auto-compaction seam with no adapter.
|
|
303
|
+
Condenser: TypeAlias = Callable[
|
|
304
|
+
[list[AgentMessage]],
|
|
305
|
+
"list[AgentMessage] | Awaitable[list[AgentMessage]]",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
310
|
+
class CondenserDeps:
|
|
311
|
+
"""Dependencies for building a :data:`Condenser`. All fields are optional
|
|
312
|
+
so a bare ``create_condenser()`` yields a working, network-free-testable
|
|
313
|
+
condenser:
|
|
314
|
+
|
|
315
|
+
- ``complete`` defaults to the framework's ``complete_simple``.
|
|
316
|
+
- ``policy`` defaults to the implementation's config-sourced thresholds.
|
|
317
|
+
- ``model`` is the summarization model; when omitted the condenser falls
|
|
318
|
+
back to a no-op (return input unchanged) rather than guess a model.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
# The injectable model completer (default: framework complete_simple).
|
|
322
|
+
# Inject a stub of `CompleteFn` shape to run the condenser w/o network.
|
|
323
|
+
complete: CompleteFn | None = None
|
|
324
|
+
|
|
325
|
+
# The model used to write the summary. When omitted, condensing is a no-op.
|
|
326
|
+
model: Model | None = None
|
|
327
|
+
|
|
328
|
+
# The config-sourced budget thresholds (default: implementation defaults).
|
|
329
|
+
policy: BudgetPolicy | None = None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Window-budget — summarization subsystem barrel (port of TS
|
|
2
|
+
``src/window-budget/summarize/index.ts``).
|
|
3
|
+
|
|
4
|
+
Model-driven condensing of the dropped transcript prefix into one synthetic
|
|
5
|
+
summary message, with re-authored prompts (new section names + a new tag
|
|
6
|
+
vocabulary). Branch-archival folds in behind the scope flag —
|
|
7
|
+
:func:`condense_scope` is the branch entrypoint over the same
|
|
8
|
+
:func:`summarize` core.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ..contract import (
|
|
12
|
+
AgentMessage,
|
|
13
|
+
CompleteFn,
|
|
14
|
+
CondenseScope,
|
|
15
|
+
Model,
|
|
16
|
+
Summary,
|
|
17
|
+
)
|
|
18
|
+
from .condense import SummarizeDeps, condense_scope, summarize
|
|
19
|
+
from .prompt import CONDENSER_BRIEF, build_summary_prompt, flatten_transcript
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AgentMessage",
|
|
23
|
+
"CONDENSER_BRIEF",
|
|
24
|
+
"CompleteFn",
|
|
25
|
+
"CondenseScope",
|
|
26
|
+
"Model",
|
|
27
|
+
"SummarizeDeps",
|
|
28
|
+
"Summary",
|
|
29
|
+
"build_summary_prompt",
|
|
30
|
+
"condense_scope",
|
|
31
|
+
"flatten_transcript",
|
|
32
|
+
"summarize",
|
|
33
|
+
]
|