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,212 @@
|
|
|
1
|
+
"""Window-budget — model-driven summarization (port of TS
|
|
2
|
+
``src/window-budget/summarize/condense.ts``).
|
|
3
|
+
|
|
4
|
+
:func:`summarize` is the single condensing primitive: given the dropped
|
|
5
|
+
prefix of a transcript and a :class:`SummarizeDeps` bundle, it flattens those
|
|
6
|
+
messages, asks the injectable model completer to write a structured digest,
|
|
7
|
+
and returns a synthetic :class:`~induscode.window_budget.contract.Summary`
|
|
8
|
+
message that stands in for the slice it covers.
|
|
9
|
+
|
|
10
|
+
Both condensing scopes flow through this one function:
|
|
11
|
+
|
|
12
|
+
- ``"session"`` — the active-session checkpoint (head condensed, tail kept by
|
|
13
|
+
the caller).
|
|
14
|
+
- ``"branch"`` — an abandoned branch archived into one message.
|
|
15
|
+
|
|
16
|
+
The only difference is the scope flag forwarded into
|
|
17
|
+
:func:`~.prompt.build_summary_prompt` (which selects the framing line).
|
|
18
|
+
:func:`condense_scope` is the thin branch-archival entrypoint that pins
|
|
19
|
+
``scope="branch"``.
|
|
20
|
+
|
|
21
|
+
The completer is injectable (:attr:`SummarizeDeps.complete`, default the
|
|
22
|
+
framework's :func:`indusagi.ai.complete_simple` — verified Python signature
|
|
23
|
+
``(model, context, options=None, logger=None) -> AssistantMessage``) so tests
|
|
24
|
+
run with no network — pass a stub that returns a canned
|
|
25
|
+
:class:`indusagi.ai.AssistantMessage`. When no ``model`` is supplied there is
|
|
26
|
+
nothing to call, so :func:`summarize` degrades to a deterministic local
|
|
27
|
+
digest rather than guessing a model — keeping it usable in a bare/offline
|
|
28
|
+
build.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, replace
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from indusagi.ai import (
|
|
38
|
+
AssistantMessage,
|
|
39
|
+
Context,
|
|
40
|
+
SimpleStreamOptions,
|
|
41
|
+
UserMessage,
|
|
42
|
+
complete_simple,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from ..contract import (
|
|
46
|
+
AgentMessage,
|
|
47
|
+
CompleteFn,
|
|
48
|
+
CondenseScope,
|
|
49
|
+
Model,
|
|
50
|
+
Summary,
|
|
51
|
+
)
|
|
52
|
+
from .prompt import CONDENSER_BRIEF, build_summary_prompt, flatten_transcript
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"SummarizeDeps",
|
|
56
|
+
"condense_scope",
|
|
57
|
+
"flatten_transcript",
|
|
58
|
+
"summarize",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Dependencies
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
68
|
+
class SummarizeDeps:
|
|
69
|
+
"""What :func:`summarize` needs. Every field is optional so the function
|
|
70
|
+
is network-free-testable: omit ``model`` for a local fallback digest, or
|
|
71
|
+
inject a ``complete`` stub for a scripted model response.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Injectable completer (default: framework complete_simple).
|
|
75
|
+
complete: CompleteFn | None = None
|
|
76
|
+
|
|
77
|
+
# The summarization model. When omitted, a deterministic local digest is used.
|
|
78
|
+
model: Model | None = None
|
|
79
|
+
|
|
80
|
+
# Which kind of condense this is; defaults to "session".
|
|
81
|
+
scope: CondenseScope = "session"
|
|
82
|
+
|
|
83
|
+
# An earlier digest to refresh/extend (iterative-refresh / branch carry-in).
|
|
84
|
+
prior_digest: str | None = None
|
|
85
|
+
|
|
86
|
+
# Cancellation signal forwarded to the completer (the framework's
|
|
87
|
+
# CancelToken — kept opaque here; it lands on SimpleStreamOptions.signal).
|
|
88
|
+
signal: Any = None
|
|
89
|
+
|
|
90
|
+
# Optional cap on the digest size, in tokens, forwarded to the
|
|
91
|
+
# completer's `maxTokens`. A modest ceiling keeps the summary itself from
|
|
92
|
+
# eating the window. When omitted the model's own default applies.
|
|
93
|
+
max_tokens: int | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Synthetic summary message
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
# The header line stamped onto the synthetic summary so a human (and the next
|
|
101
|
+
# condense pass, via the `digest:` flattener case) can tell it apart from a
|
|
102
|
+
# real user turn. Re-authored wording (verbatim from the TS rebuild) — not
|
|
103
|
+
# the legacy `<summary>` framing, and DISTINCT from the framework
|
|
104
|
+
# `indusagi.runtime.memory` compactor's `[condensed earlier context]` heading.
|
|
105
|
+
_DIGEST_HEADER = "[session digest — older turns condensed]"
|
|
106
|
+
_BRANCH_HEADER = "[branch digest — archived from a path not taken]"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _to_summary_message(digest: str, scope: CondenseScope) -> AgentMessage:
|
|
110
|
+
"""Wrap digest text into a synthetic :data:`AgentMessage`. Modeled as a
|
|
111
|
+
plain ``user``-role message carrying the digest as text: it is the
|
|
112
|
+
simplest legal ``AgentMessage``, passes through the LLM conversion
|
|
113
|
+
natively, and the conductor splices it straight back into the message
|
|
114
|
+
list with no special handling."""
|
|
115
|
+
header = _BRANCH_HEADER if scope == "branch" else _DIGEST_HEADER
|
|
116
|
+
return UserMessage(
|
|
117
|
+
content=f"{header}\n\n{digest}",
|
|
118
|
+
timestamp=int(time.time() * 1000),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _text_of(message: AssistantMessage) -> str:
|
|
123
|
+
"""Extract the concatenated text from a completer's assistant message."""
|
|
124
|
+
parts: list[str] = []
|
|
125
|
+
for block in message.content:
|
|
126
|
+
if getattr(block, "type", None) == "text" and block.text.strip():
|
|
127
|
+
parts.append(block.text)
|
|
128
|
+
return "\n".join(parts).strip()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _local_fallback_digest(
|
|
132
|
+
messages: list[AgentMessage], prior_digest: str | None = None
|
|
133
|
+
) -> str:
|
|
134
|
+
"""The offline fallback digest used when no ``model`` is supplied. It is
|
|
135
|
+
not a model summary — it simply records that the slice was elided and
|
|
136
|
+
preserves the prior digest if one was carried in, so a bare build never
|
|
137
|
+
silently loses the marker."""
|
|
138
|
+
lines = [
|
|
139
|
+
"# Carryover",
|
|
140
|
+
f"{len(messages)} earlier message(s) were elided without a summarization model bound.",
|
|
141
|
+
]
|
|
142
|
+
if prior_digest and prior_digest.strip():
|
|
143
|
+
lines.extend(["", prior_digest.strip()])
|
|
144
|
+
return "\n".join(lines)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Core
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def summarize(
|
|
153
|
+
messages: list[AgentMessage],
|
|
154
|
+
deps: SummarizeDeps | None = None,
|
|
155
|
+
) -> Summary:
|
|
156
|
+
"""Summarize a slice of dropped messages into a single synthetic
|
|
157
|
+
:class:`~induscode.window_budget.contract.Summary`.
|
|
158
|
+
|
|
159
|
+
One code path for both scopes — ``deps.scope`` (default ``"session"``)
|
|
160
|
+
only changes the prompt framing. With a ``model`` bound it calls
|
|
161
|
+
``deps.complete`` (default :func:`indusagi.ai.complete_simple`) once with
|
|
162
|
+
:data:`~.prompt.CONDENSER_BRIEF` as the system prompt and
|
|
163
|
+
:func:`~.prompt.build_summary_prompt` as the user turn; otherwise it
|
|
164
|
+
returns a deterministic local digest. Never raises on an empty model
|
|
165
|
+
reply — it falls back to the local digest so a ``Summary`` is always
|
|
166
|
+
produced.
|
|
167
|
+
|
|
168
|
+
:returns: a ``Summary`` whose ``message`` replaces the ``covered_count``
|
|
169
|
+
source messages it summarizes.
|
|
170
|
+
"""
|
|
171
|
+
if deps is None:
|
|
172
|
+
deps = SummarizeDeps()
|
|
173
|
+
scope: CondenseScope = deps.scope
|
|
174
|
+
covered_count = len(messages)
|
|
175
|
+
|
|
176
|
+
# No model bound → deterministic, network-free local digest.
|
|
177
|
+
if deps.model is None:
|
|
178
|
+
digest = _local_fallback_digest(messages, deps.prior_digest)
|
|
179
|
+
return Summary(message=_to_summary_message(digest, scope), covered_count=covered_count)
|
|
180
|
+
|
|
181
|
+
prompt = build_summary_prompt(messages, scope, deps.prior_digest)
|
|
182
|
+
context = Context(
|
|
183
|
+
systemPrompt=CONDENSER_BRIEF,
|
|
184
|
+
messages=[UserMessage(content=prompt, timestamp=int(time.time() * 1000))],
|
|
185
|
+
)
|
|
186
|
+
options = SimpleStreamOptions(reasoning="high")
|
|
187
|
+
if deps.max_tokens is not None:
|
|
188
|
+
options.maxTokens = deps.max_tokens
|
|
189
|
+
if deps.signal is not None:
|
|
190
|
+
options.signal = deps.signal
|
|
191
|
+
|
|
192
|
+
complete: CompleteFn = deps.complete if deps.complete is not None else complete_simple
|
|
193
|
+
reply = await complete(deps.model, context, options)
|
|
194
|
+
|
|
195
|
+
digest = _text_of(reply)
|
|
196
|
+
final_digest = digest or _local_fallback_digest(messages, deps.prior_digest)
|
|
197
|
+
return Summary(
|
|
198
|
+
message=_to_summary_message(final_digest, scope),
|
|
199
|
+
covered_count=covered_count,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def condense_scope(
|
|
204
|
+
messages: list[AgentMessage],
|
|
205
|
+
deps: SummarizeDeps | None = None,
|
|
206
|
+
) -> Summary:
|
|
207
|
+
"""Branch-archival entrypoint: condense an abandoned branch into one
|
|
208
|
+
summary message. A thin wrapper that pins ``scope="branch"`` and
|
|
209
|
+
delegates to :func:`summarize` — the branch case is the same machinery,
|
|
210
|
+
not a separate engine."""
|
|
211
|
+
base = deps if deps is not None else SummarizeDeps()
|
|
212
|
+
return await summarize(messages, replace(base, scope="branch"))
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Window-budget — summarization prompt assembly (port of TS
|
|
2
|
+
``src/window-budget/summarize/prompt.ts``; re-authored prompt, copied
|
|
3
|
+
VERBATIM here).
|
|
4
|
+
|
|
5
|
+
When the transcript crosses budget, the older head is *condensed* into one
|
|
6
|
+
synthetic message. This module turns the dropped slice of
|
|
7
|
+
:data:`~induscode.window_budget.contract.AgentMessage` s into the single text
|
|
8
|
+
payload handed to the model completer. Two pieces ship here:
|
|
9
|
+
|
|
10
|
+
1. :data:`CONDENSER_BRIEF` — the system-prompt brief that frames the model as
|
|
11
|
+
a *recorder*, not a continuation of the chat: read the transcript as data
|
|
12
|
+
and emit only the structured digest.
|
|
13
|
+
2. :func:`build_summary_prompt` — flattens the dropped messages into a tagged
|
|
14
|
+
block and appends the section template the model fills in.
|
|
15
|
+
|
|
16
|
+
OWN VOCABULARY (an original prompt design):
|
|
17
|
+
|
|
18
|
+
- Section headings are ``# Objective / # Guardrails /
|
|
19
|
+
# Status (Shipped / Active / Stuck) / # Rationale / # Plan / # Carryover``.
|
|
20
|
+
- The transcript is wrapped in ``<scrollback>…</scrollback>`` and a prior
|
|
21
|
+
digest (the branch-archival / iterative-refresh path) in
|
|
22
|
+
``<carried-digest>…</carried-digest>``.
|
|
23
|
+
- Per-message lines use ``» role:`` markers (``» you``, ``» agent``,
|
|
24
|
+
``» agent.plan``, ``» agent.call``, ``» tool``).
|
|
25
|
+
|
|
26
|
+
No token math, no model calls, no I/O here — only string assembly. The scope
|
|
27
|
+
flag selects the framing line (active-session checkpoint vs abandoned-branch
|
|
28
|
+
archive); everything else is one code path.
|
|
29
|
+
|
|
30
|
+
Port note: the flattener's role dispatch covers the agent's custom session
|
|
31
|
+
messages (``bashExecution`` / ``custom`` / ``branchSummary`` /
|
|
32
|
+
``compactionSummary``) EXPLICITLY — the Python ``indusagi.ai.AgentMessage``
|
|
33
|
+
alias excludes them (analysis 05 risk-2), and probing is ``getattr`` duck
|
|
34
|
+
typing over the frozen message dataclasses, not dict access.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
from ..contract import AgentMessage, CondenseScope
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"CONDENSER_BRIEF",
|
|
46
|
+
"build_summary_prompt",
|
|
47
|
+
"flatten_transcript",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# System brief
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
#: The system-prompt brief for the condenser model. Frames the request as a
|
|
56
|
+
#: record-keeping task — the model must NOT answer, continue, or act on the
|
|
57
|
+
#: conversation; it only transcribes it into the fixed section layout.
|
|
58
|
+
#: Re-authored wording (copied verbatim from the TS rebuild); shares no
|
|
59
|
+
#: sentences with the legacy ``SUMMARIZATION_SYSTEM_PROMPT``.
|
|
60
|
+
CONDENSER_BRIEF = "\n".join(
|
|
61
|
+
[
|
|
62
|
+
"You are a transcript recorder for a long-running coding session.",
|
|
63
|
+
"The text you receive is archival data, not a live chat — do NOT reply to it,",
|
|
64
|
+
"continue it, run tools, or ask questions. Your sole job is to distill it into",
|
|
65
|
+
"a compact, faithful digest under the exact headings requested below.",
|
|
66
|
+
"",
|
|
67
|
+
"Rules:",
|
|
68
|
+
"- Preserve concrete facts: file paths, identifiers, commands, error text,",
|
|
69
|
+
" decisions, and anything the session would need to resume without the",
|
|
70
|
+
" original turns.",
|
|
71
|
+
"- Prefer specifics over paraphrase; never invent details that are not present.",
|
|
72
|
+
"- Keep each heading even if a section is empty (write `none` under it).",
|
|
73
|
+
"- Emit only the digest — no preamble, no sign-off, no commentary.",
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Transcript flattening
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
# Marker prefix for every flattened turn line.
|
|
83
|
+
_TURN_MARK = "»"
|
|
84
|
+
|
|
85
|
+
_TRAILING_WS_RE = re.compile(r"[ \t]+\n")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _tidy(text: str) -> str:
|
|
89
|
+
"""Collapse runs of whitespace so the flattened block stays compact."""
|
|
90
|
+
return _TRAILING_WS_RE.sub("\n", text.replace("\r\n", "\n")).strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_content(content: object) -> str:
|
|
94
|
+
"""Pull plain text out of a string-or-content-blocks field."""
|
|
95
|
+
if isinstance(content, str):
|
|
96
|
+
return content
|
|
97
|
+
parts: list[str] = []
|
|
98
|
+
if isinstance(content, (list, tuple)):
|
|
99
|
+
for block in content:
|
|
100
|
+
block_type = getattr(block, "type", None)
|
|
101
|
+
if block_type == "text":
|
|
102
|
+
text = getattr(block, "text", None)
|
|
103
|
+
if isinstance(text, str):
|
|
104
|
+
parts.append(text)
|
|
105
|
+
elif block_type == "image":
|
|
106
|
+
parts.append("[image]")
|
|
107
|
+
return "\n".join(parts)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _safe_json(value: object) -> str:
|
|
111
|
+
"""JSON-serialize that never raises and never explodes the line width."""
|
|
112
|
+
try:
|
|
113
|
+
return json.dumps(value, default=str, separators=(",", ":"))
|
|
114
|
+
except Exception:
|
|
115
|
+
return "{}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _flatten_message(message: AgentMessage) -> str:
|
|
119
|
+
"""Render a single :data:`AgentMessage` as one-or-more ``» role:`` lines.
|
|
120
|
+
Returns ``""`` for messages with no projectable content (they are
|
|
121
|
+
skipped). This is the re-authored flattener — role markers and ordering
|
|
122
|
+
differ from the legacy ``serializeConversation``.
|
|
123
|
+
"""
|
|
124
|
+
role = getattr(message, "role", None)
|
|
125
|
+
|
|
126
|
+
if role == "user":
|
|
127
|
+
text = _tidy(_read_content(getattr(message, "content", None)))
|
|
128
|
+
return f"{_TURN_MARK} you: {text}" if text else ""
|
|
129
|
+
|
|
130
|
+
if role == "assistant":
|
|
131
|
+
lines: list[str] = []
|
|
132
|
+
content = getattr(message, "content", None) or ()
|
|
133
|
+
for block in content:
|
|
134
|
+
block_type = getattr(block, "type", None)
|
|
135
|
+
if block_type == "text" and getattr(block, "text", "").strip():
|
|
136
|
+
lines.append(f"{_TURN_MARK} agent: {_tidy(block.text)}")
|
|
137
|
+
elif block_type == "thinking" and getattr(block, "thinking", "").strip():
|
|
138
|
+
lines.append(f"{_TURN_MARK} agent.plan: {_tidy(block.thinking)}")
|
|
139
|
+
elif block_type == "toolCall":
|
|
140
|
+
args = _safe_json(getattr(block, "arguments", None))
|
|
141
|
+
lines.append(f"{_TURN_MARK} agent.call {block.name}: {args}")
|
|
142
|
+
return "\n".join(lines)
|
|
143
|
+
|
|
144
|
+
if role == "toolResult":
|
|
145
|
+
text = _tidy(_read_content(getattr(message, "content", None)))
|
|
146
|
+
tag = "tool!err" if getattr(message, "isError", False) else "tool"
|
|
147
|
+
tool_name = getattr(message, "toolName", "")
|
|
148
|
+
return f"{_TURN_MARK} {tag} ({tool_name}): {text}"
|
|
149
|
+
|
|
150
|
+
if role == "bashExecution":
|
|
151
|
+
out = _tidy(getattr(message, "output", "") or "")
|
|
152
|
+
exit_code = getattr(message, "exitCode", None)
|
|
153
|
+
code = exit_code if exit_code is not None else "n/a"
|
|
154
|
+
command = _tidy(getattr(message, "command", "") or "")
|
|
155
|
+
return f"{_TURN_MARK} shell$ {command} [exit {code}]\n{out}"
|
|
156
|
+
|
|
157
|
+
if role == "custom":
|
|
158
|
+
text = _tidy(_read_content(getattr(message, "content", None)))
|
|
159
|
+
custom_type = getattr(message, "customType", "")
|
|
160
|
+
return f"{_TURN_MARK} note ({custom_type}): {text}" if text else ""
|
|
161
|
+
|
|
162
|
+
if role in ("branchSummary", "compactionSummary"):
|
|
163
|
+
text = _tidy(getattr(message, "summary", "") or "")
|
|
164
|
+
return f"{_TURN_MARK} digest: {text}" if text else ""
|
|
165
|
+
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def flatten_transcript(messages: list[AgentMessage]) -> str:
|
|
170
|
+
"""Flatten a list of messages into a single ``\\n``-joined block of
|
|
171
|
+
``» role:`` lines. Exported so the condenser can reuse it for
|
|
172
|
+
diagnostics/tests."""
|
|
173
|
+
lines: list[str] = []
|
|
174
|
+
for message in messages:
|
|
175
|
+
rendered = _flatten_message(message)
|
|
176
|
+
if rendered:
|
|
177
|
+
lines.append(rendered)
|
|
178
|
+
return "\n".join(lines)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Prompt builder
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
# The re-authored section template the model fills in (verbatim from the TS
|
|
186
|
+
# rebuild — these headings are pinned by tests and the digest parser).
|
|
187
|
+
_SECTION_TEMPLATE = "\n".join(
|
|
188
|
+
[
|
|
189
|
+
"# Objective",
|
|
190
|
+
"# Guardrails",
|
|
191
|
+
"# Status (Shipped / Active / Stuck)",
|
|
192
|
+
"# Rationale",
|
|
193
|
+
"# Plan",
|
|
194
|
+
"# Carryover",
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _framing(scope: CondenseScope) -> str:
|
|
200
|
+
"""The scope-specific framing line that precedes the template."""
|
|
201
|
+
if scope == "branch":
|
|
202
|
+
return (
|
|
203
|
+
"Archive this abandoned branch so it can be folded back as context later. "
|
|
204
|
+
"Distill it under these headings:"
|
|
205
|
+
)
|
|
206
|
+
return (
|
|
207
|
+
"Condense the older portion of this active session so recent turns stay verbatim. "
|
|
208
|
+
"Distill it under these headings:"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_summary_prompt(
|
|
213
|
+
messages: list[AgentMessage],
|
|
214
|
+
scope: CondenseScope,
|
|
215
|
+
prior_digest: str | None = None,
|
|
216
|
+
) -> str:
|
|
217
|
+
"""Build the full summarization prompt for a slice of dropped messages.
|
|
218
|
+
|
|
219
|
+
The result is one string: the scope framing, the flattened transcript
|
|
220
|
+
wrapped in ``<scrollback>``, an optional carried digest wrapped in
|
|
221
|
+
``<carried-digest>`` (the iterative-refresh / branch-archival input), and
|
|
222
|
+
the section template the model fills in. Pass this as the user turn to
|
|
223
|
+
the injectable completer; pair it with :data:`CONDENSER_BRIEF` as the
|
|
224
|
+
system prompt.
|
|
225
|
+
|
|
226
|
+
:param messages: the dropped prefix to summarize
|
|
227
|
+
:param scope: ``"session"`` (active checkpoint) or ``"branch"`` (archive)
|
|
228
|
+
:param prior_digest: an optional earlier digest to refresh/extend,
|
|
229
|
+
threaded in under ``<carried-digest>`` so the model merges rather
|
|
230
|
+
than re-derives. Omit for a first-pass condense.
|
|
231
|
+
"""
|
|
232
|
+
transcript = flatten_transcript(messages)
|
|
233
|
+
blocks: list[str] = [_framing(scope)]
|
|
234
|
+
|
|
235
|
+
if prior_digest and prior_digest.strip():
|
|
236
|
+
blocks.append(f"<carried-digest>\n{_tidy(prior_digest)}\n</carried-digest>")
|
|
237
|
+
|
|
238
|
+
blocks.append(f"<scrollback>\n{transcript}\n</scrollback>")
|
|
239
|
+
blocks.append(_SECTION_TEMPLATE)
|
|
240
|
+
|
|
241
|
+
return "\n\n".join(blocks)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Workspace — product identity (:data:`BRAND`, :data:`VERSION`) and the
|
|
2
|
+
resolved on-disk layout (:class:`Workspace` via :func:`create_workspace`).
|
|
3
|
+
|
|
4
|
+
Port of the TS ``src/workspace`` barrel (``runtime-detect.ts`` dropped; see
|
|
5
|
+
the :mod:`~induscode.workspace.locator` module docstring for why).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .brand import BRAND, Brand, VERSION
|
|
9
|
+
from .locator import (
|
|
10
|
+
DIRECTORY_KEYS,
|
|
11
|
+
ENV_FRAMEWORK_HOME,
|
|
12
|
+
LAYOUT,
|
|
13
|
+
Workspace,
|
|
14
|
+
WorkspaceOverrides,
|
|
15
|
+
create_workspace,
|
|
16
|
+
ensure_dirs,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BRAND",
|
|
21
|
+
"Brand",
|
|
22
|
+
"DIRECTORY_KEYS",
|
|
23
|
+
"ENV_FRAMEWORK_HOME",
|
|
24
|
+
"LAYOUT",
|
|
25
|
+
"VERSION",
|
|
26
|
+
"Workspace",
|
|
27
|
+
"WorkspaceOverrides",
|
|
28
|
+
"create_workspace",
|
|
29
|
+
"ensure_dirs",
|
|
30
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Brand record — the single source of truth for every identity literal.
|
|
2
|
+
|
|
3
|
+
One frozen :class:`Brand` value. Nothing else in the app hard-codes the
|
|
4
|
+
product name, the bin names, the env-var namespace, or the share-viewer
|
|
5
|
+
origin. A rebrand edits this record and nothing else.
|
|
6
|
+
|
|
7
|
+
Port note (TS ``src/workspace/brand.ts``)
|
|
8
|
+
-----------------------------------------
|
|
9
|
+
The TS record carried ``profileDirName: ".indusagi"`` plus a nested
|
|
10
|
+
``stateDirName: "agent"`` (profile root ``~/.indusagi/agent``). The Python
|
|
11
|
+
build owns the flat ``~/.pindusagi`` root shared with the ``indusagi``
|
|
12
|
+
framework — there is no ``agent/`` nesting because nothing else shares the
|
|
13
|
+
directory — so ``state_dir_name`` is dropped and :attr:`Brand.profile_dir_name`
|
|
14
|
+
is composed from the framework's own brand record
|
|
15
|
+
(:data:`indusagi.shell_app.locate.BRAND`) so the two can never drift.
|
|
16
|
+
|
|
17
|
+
The bin names are the locked Python renames (``pindus``/``induscode``; npm
|
|
18
|
+
owns ``indus``/``indusagi``). The env-var names and the share-viewer fields
|
|
19
|
+
are kept verbatim from TS.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from importlib import metadata as _metadata
|
|
26
|
+
from typing import Final
|
|
27
|
+
|
|
28
|
+
from indusagi.shell_app.locate import BRAND as _FRAMEWORK_BRAND
|
|
29
|
+
|
|
30
|
+
__all__ = ["BRAND", "Brand", "VERSION"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class Brand:
|
|
35
|
+
"""The shape of the brand record.
|
|
36
|
+
|
|
37
|
+
Field names are the mechanical snake_case renames of the TS ``Brand``
|
|
38
|
+
contract (``boot/contract.ts``); values are the induscode identity.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# The product's machine name — printed by `--version`, stamped on traces.
|
|
42
|
+
name: str
|
|
43
|
+
# The product's display label, used in banners and prose.
|
|
44
|
+
label: str
|
|
45
|
+
# The dot-directory carved under the user's home for all per-user state
|
|
46
|
+
# (e.g. ".pindusagi"). Composed from the framework brand; the Python build
|
|
47
|
+
# shares the framework's flat root (no TS-style "agent/" nesting).
|
|
48
|
+
profile_dir_name: str
|
|
49
|
+
# The console-script names users invoke (primary first).
|
|
50
|
+
bin_names: tuple[str, ...]
|
|
51
|
+
# The namespace prefix on every environment variable the app reads.
|
|
52
|
+
env_prefix: str
|
|
53
|
+
# Env var that relocates the entire agent profile directory.
|
|
54
|
+
env_profile_dir: str
|
|
55
|
+
# Env var that enables verbose diagnostics.
|
|
56
|
+
env_debug: str
|
|
57
|
+
# Env var that overrides the share-viewer origin.
|
|
58
|
+
env_share_viewer: str
|
|
59
|
+
# Default origin shared transcripts are viewed at.
|
|
60
|
+
share_viewer_url: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
#: The concrete induscode identity. Frozen so the record cannot be mutated by
|
|
64
|
+
#: any consumer; a drift from the contract is a construction error here rather
|
|
65
|
+
#: than a runtime surprise downstream.
|
|
66
|
+
BRAND: Final[Brand] = Brand(
|
|
67
|
+
name="induscode",
|
|
68
|
+
label="induscode",
|
|
69
|
+
profile_dir_name=_FRAMEWORK_BRAND.profile_dir_name, # ".pindusagi"
|
|
70
|
+
bin_names=("pindus", "induscode"),
|
|
71
|
+
env_prefix="INDUSAGI",
|
|
72
|
+
env_profile_dir="INDUSAGI_CODING_AGENT_DIR",
|
|
73
|
+
env_debug="INDUSAGI_DEBUG",
|
|
74
|
+
env_share_viewer="INDUSAGI_SHARE_VIEWER_URL",
|
|
75
|
+
share_viewer_url="https://buildwithindusagi.ai/session/",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _detect_version() -> str:
|
|
80
|
+
"""Resolve the product version from the installed package metadata.
|
|
81
|
+
|
|
82
|
+
Single-sourced from ``pyproject.toml`` via :mod:`importlib.metadata` —
|
|
83
|
+
the TS build duplicated the version as a literal (``VERSION = "0.1.62"``
|
|
84
|
+
in ``brand.ts`` next to ``package.json``); the port fixes that. The
|
|
85
|
+
fallback only fires when the package is imported without being installed
|
|
86
|
+
(e.g. straight off a source checkout's ``sys.path``).
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
return _metadata.version("induscode")
|
|
90
|
+
except _metadata.PackageNotFoundError: # pragma: no cover - dev checkout only
|
|
91
|
+
return "0.1.0.dev0"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
#: The product version — printed by `--version` and shown on the startup
|
|
95
|
+
#: banner. (TS lineage: indusagi-coding-agent v0.1.62.)
|
|
96
|
+
VERSION: Final[str] = _detect_version()
|