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,414 @@
|
|
|
1
|
+
"""Briefing composer — the declarative system-prompt pipeline.
|
|
2
|
+
|
|
3
|
+
The system prompt is assembled from an ordered list of
|
|
4
|
+
:class:`~induscode.briefing.contract.BriefingSection` descriptors, not a string
|
|
5
|
+
template with ``{{TOKEN}}`` holes. Each section decides for itself whether it
|
|
6
|
+
contributes to a given :class:`~induscode.briefing.contract.BriefingContext`
|
|
7
|
+
(its ``applies`` predicate) and renders its own fragment (its ``render``). The
|
|
8
|
+
composer is a small reducer: it walks the recipe in order, keeps the
|
|
9
|
+
applicable sections, renders them, drops empties, and joins with blank-line
|
|
10
|
+
gaps. Adding, removing, or reordering a section is a data edit to
|
|
11
|
+
:data:`BRIEFING_SECTIONS`, never a change to the composer.
|
|
12
|
+
|
|
13
|
+
Every guideline, tool note, and heading below is authored for this rebuild
|
|
14
|
+
(the prose is copied verbatim from the TS ``src/briefing/compose.ts``). The
|
|
15
|
+
*set* of sections (role, tools, working guidance, task tracking, delegates,
|
|
16
|
+
connectors, project context, skills, footer) follows the well-known coding-
|
|
17
|
+
agent shape; the wording is the briefing's own.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Final, Mapping
|
|
25
|
+
|
|
26
|
+
from .contract import (
|
|
27
|
+
AgentTool,
|
|
28
|
+
Briefing,
|
|
29
|
+
BriefingContext,
|
|
30
|
+
BriefingInputs,
|
|
31
|
+
BriefingSection,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BRIEFING_SECTIONS",
|
|
36
|
+
"compose_briefing",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Small render helpers
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _join_blocks(parts: Sequence[str]) -> str:
|
|
46
|
+
"""Join non-empty fragments with a blank line between them."""
|
|
47
|
+
return "\n\n".join(p for p in parts if p.strip())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _bullets(lines: Sequence[str]) -> str:
|
|
51
|
+
"""Render a markdown bullet list from already-formatted line bodies."""
|
|
52
|
+
return "\n".join(f"- {line}" for line in lines)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _escape_xml(value: str) -> str:
|
|
56
|
+
"""Escape the five XML metacharacters for the skills block's tag values."""
|
|
57
|
+
out = ""
|
|
58
|
+
for ch in value:
|
|
59
|
+
if ch == "&":
|
|
60
|
+
out += "&"
|
|
61
|
+
elif ch == "<":
|
|
62
|
+
out += "<"
|
|
63
|
+
elif ch == ">":
|
|
64
|
+
out += ">"
|
|
65
|
+
elif ch == '"':
|
|
66
|
+
out += """
|
|
67
|
+
elif ch == "'":
|
|
68
|
+
out += "'"
|
|
69
|
+
else:
|
|
70
|
+
out += ch
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _has_tool(ctx: BriefingContext, tool_id: str) -> bool:
|
|
75
|
+
"""True when the context advertises a tool whose name matches ``tool_id``."""
|
|
76
|
+
return any(t.name == tool_id for t in ctx.tools or ())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _has_tool_prefix(ctx: BriefingContext, prefix: str) -> bool:
|
|
80
|
+
"""True when the context advertises any tool whose name begins with
|
|
81
|
+
``prefix``."""
|
|
82
|
+
return any(t.name.startswith(prefix) for t in ctx.tools or ())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _iso_utc(moment: datetime) -> str:
|
|
86
|
+
"""Format a timestamp the way the TS footer did (``Date.toISOString()``):
|
|
87
|
+
UTC, millisecond precision, trailing ``Z``. A naive datetime is treated as
|
|
88
|
+
already-UTC."""
|
|
89
|
+
if moment.tzinfo is not None:
|
|
90
|
+
moment = moment.astimezone(timezone.utc)
|
|
91
|
+
return moment.strftime("%Y-%m-%dT%H:%M:%S.") + f"{moment.microsecond // 1000:03d}Z"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Tool descriptions (re-authored one-liners)
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
#: One-line summaries for the built-in tools, keyed by tool name. The briefing
|
|
99
|
+
#: surfaces the summary for each advertised tool that has one; tools without
|
|
100
|
+
#: an entry fall back to their own ``description`` from the
|
|
101
|
+
#: :class:`~induscode.briefing.contract.AgentTool` record.
|
|
102
|
+
#:
|
|
103
|
+
#: The phrasing here is the briefing's own.
|
|
104
|
+
TOOL_SUMMARIES: Final[Mapping[str, str]] = {
|
|
105
|
+
"read": "Open a file's contents for inspection.",
|
|
106
|
+
"write": "Create a new file or overwrite an existing one wholesale.",
|
|
107
|
+
"edit": "Apply a precise in-place change by matching exact existing text.",
|
|
108
|
+
"bash": "Run a shell command in the workspace.",
|
|
109
|
+
"grep": "Search file contents by pattern across the tree.",
|
|
110
|
+
"find": "Locate files and directories by name or glob.",
|
|
111
|
+
"ls": "List the entries of a directory.",
|
|
112
|
+
"task": "Hand a self-contained sub-task to a delegate agent.",
|
|
113
|
+
"todoread": "Read back the current task checklist.",
|
|
114
|
+
"todowrite": "Record or revise the task checklist.",
|
|
115
|
+
"webfetch": "Retrieve and read the contents of a URL.",
|
|
116
|
+
"websearch": "Query the web for current information.",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _describe_tool(tool: AgentTool) -> str:
|
|
121
|
+
"""Render a single tool's advertised line: name + best available summary."""
|
|
122
|
+
summary = TOOL_SUMMARIES.get(tool.name)
|
|
123
|
+
if summary is None:
|
|
124
|
+
description = getattr(tool, "description", None)
|
|
125
|
+
summary = description.strip() if isinstance(description, str) else ""
|
|
126
|
+
return f"`{tool.name}` — {summary}" if summary else f"`{tool.name}`"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Sections
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _render_role(_ctx: BriefingContext) -> str:
|
|
135
|
+
return _join_blocks(
|
|
136
|
+
[
|
|
137
|
+
"You are a terminal-based software engineering assistant. You operate inside a real workspace and make progress by reading code, running commands, and editing files directly — not by describing what someone else should do.",
|
|
138
|
+
"Work toward the user's actual goal. Take the initiative to gather the context you need, but stay within the scope of what was asked: do not redesign, rename, or refactor beyond the request unless the user invites it.",
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
#: Role / setup. Always present. Establishes the agent's purpose and the
|
|
144
|
+
#: stance the rest of the briefing assumes.
|
|
145
|
+
ROLE_SECTION: Final[BriefingSection] = BriefingSection(id="role", render=_render_role)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _tools_applies(ctx: BriefingContext) -> bool:
|
|
149
|
+
return len(ctx.tools or ()) > 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _render_tools(ctx: BriefingContext) -> str:
|
|
153
|
+
lines = [_describe_tool(t) for t in ctx.tools or ()]
|
|
154
|
+
return _join_blocks(
|
|
155
|
+
[
|
|
156
|
+
"# Tools",
|
|
157
|
+
"These capabilities are available to you this turn. Reach for the most specific one for the job.",
|
|
158
|
+
_bullets(lines),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
#: Available tools. Renders only when the context advertises at least one
|
|
164
|
+
#: tool. Lists each advertised tool with its re-authored one-liner.
|
|
165
|
+
TOOLS_SECTION: Final[BriefingSection] = BriefingSection(
|
|
166
|
+
id="tools", title="Tools", applies=_tools_applies, render=_render_tools
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _render_guidelines(ctx: BriefingContext) -> str:
|
|
171
|
+
items: list[str] = []
|
|
172
|
+
|
|
173
|
+
if _has_tool(ctx, "read"):
|
|
174
|
+
items.append(
|
|
175
|
+
"Always inspect a file with the reader before you change it. Do not shell out to `cat`, `head`, or `sed` to view a file when the reader is available."
|
|
176
|
+
)
|
|
177
|
+
if _has_tool(ctx, "edit"):
|
|
178
|
+
items.append(
|
|
179
|
+
"For a targeted change, prefer the editor: it replaces an exact span of existing text, so the old text you supply must match the file byte-for-byte."
|
|
180
|
+
)
|
|
181
|
+
if _has_tool(ctx, "write"):
|
|
182
|
+
items.append(
|
|
183
|
+
"Reserve the writer for creating a new file or replacing one in full; for anything smaller, edit in place rather than rewriting the whole file."
|
|
184
|
+
)
|
|
185
|
+
if _has_tool(ctx, "grep") or _has_tool(ctx, "find") or _has_tool(ctx, "ls"):
|
|
186
|
+
items.append(
|
|
187
|
+
"Explore the tree with the dedicated search and listing tools rather than shell equivalents — they are quicker and already skip ignored paths."
|
|
188
|
+
)
|
|
189
|
+
if _has_tool(ctx, "bash"):
|
|
190
|
+
items.append(
|
|
191
|
+
"Use the shell for builds, tests, and other commands that no purpose-built tool covers. Keep commands scoped and avoid destructive operations unless the user asked for them."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
items.append(
|
|
195
|
+
"Keep your replies short and to the point. Lead with the result; add only the explanation the user needs to act on it."
|
|
196
|
+
)
|
|
197
|
+
items.append(
|
|
198
|
+
"When a change is non-trivial, verify it — run the build or the relevant tests — before you report it as done."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return _join_blocks(["# Working guidance", _bullets(items)])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
#: Working guidance. The behavioral guidelines — every sentence authored
|
|
205
|
+
#: fresh. Each guideline is gated on the relevant tool being present so the
|
|
206
|
+
#: briefing never instructs the model to use a capability it was not given.
|
|
207
|
+
GUIDELINES_SECTION: Final[BriefingSection] = BriefingSection(
|
|
208
|
+
id="guidelines", title="Working guidance", render=_render_guidelines
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _tasks_applies(ctx: BriefingContext) -> bool:
|
|
213
|
+
return _has_tool(ctx, "todowrite") or _has_tool(ctx, "todoread")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _render_tasks(_ctx: BriefingContext) -> str:
|
|
217
|
+
return _join_blocks(
|
|
218
|
+
[
|
|
219
|
+
"# Task tracking",
|
|
220
|
+
"For work that spans several steps, maintain a checklist with the task tools. Break the job into concrete items, mark exactly one in progress at a time, and tick items off as you finish them so the user can follow along.",
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
#: Task tracking. Present when a todo tool is advertised. Explains how the
|
|
226
|
+
#: model should keep the shared checklist current.
|
|
227
|
+
TASK_SECTION: Final[BriefingSection] = BriefingSection(
|
|
228
|
+
id="tasks", title="Task tracking", applies=_tasks_applies, render=_render_tasks
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _subagents_applies(ctx: BriefingContext) -> bool:
|
|
233
|
+
return len(ctx.subagents or ()) > 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _render_subagents(ctx: BriefingContext) -> str:
|
|
237
|
+
lines = []
|
|
238
|
+
for s in ctx.subagents or ():
|
|
239
|
+
when = f" Use it when {s.when}" if s.when else ""
|
|
240
|
+
lines.append(f"**{s.name}** — {s.purpose}.{when}")
|
|
241
|
+
return _join_blocks(
|
|
242
|
+
[
|
|
243
|
+
"# Delegates",
|
|
244
|
+
"You can offload a focused, self-contained piece of work to one of these delegates with the task tool. Hand off when a sub-problem is well-scoped enough to be solved without your full conversation context.",
|
|
245
|
+
_bullets(lines),
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
#: Delegates / subagents. Present when the context lists delegate roles.
|
|
251
|
+
#: Renders each role the primary agent may hand work to.
|
|
252
|
+
SUBAGENTS_SECTION: Final[BriefingSection] = BriefingSection(
|
|
253
|
+
id="subagents", title="Delegates", applies=_subagents_applies, render=_render_subagents
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _connectors_applies(ctx: BriefingContext) -> bool:
|
|
258
|
+
return _has_tool_prefix(ctx, "connector_") or _has_tool_prefix(ctx, "saas_")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _render_connectors(_ctx: BriefingContext) -> str:
|
|
262
|
+
return _join_blocks(
|
|
263
|
+
[
|
|
264
|
+
"# Connectors",
|
|
265
|
+
_bullets(
|
|
266
|
+
[
|
|
267
|
+
"When a task needs an external service (issue trackers, calendars, docs, and the like), prefer the connector tools over scraping or guessing.",
|
|
268
|
+
"Confirm the connection is authorized before you rely on it, and surface a clear next step if it is not.",
|
|
269
|
+
"Page through large result sets deliberately rather than assuming the first response is complete.",
|
|
270
|
+
]
|
|
271
|
+
),
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
#: Connector guidance. Present when SaaS-connector tools (prefixed) are
|
|
277
|
+
#: advertised. Authored fresh; no carried-over vendor prose.
|
|
278
|
+
CONNECTORS_SECTION: Final[BriefingSection] = BriefingSection(
|
|
279
|
+
id="connectors", title="Connectors", applies=_connectors_applies, render=_render_connectors
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _project_context_applies(ctx: BriefingContext) -> bool:
|
|
284
|
+
return len(ctx.context_docs or ()) > 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _render_project_context(ctx: BriefingContext) -> str:
|
|
288
|
+
docs = [f"## {d.path}\n\n{d.body.strip()}" for d in ctx.context_docs or ()]
|
|
289
|
+
return _join_blocks(
|
|
290
|
+
[
|
|
291
|
+
"# Project context",
|
|
292
|
+
"The following project documents describe conventions for this repository. Treat them as standing instructions.",
|
|
293
|
+
*docs,
|
|
294
|
+
]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
#: Project context. Present when the context carries project documents.
|
|
299
|
+
#: Inlines each under its own sub-heading so repository conventions are in
|
|
300
|
+
#: view.
|
|
301
|
+
PROJECT_CONTEXT_SECTION: Final[BriefingSection] = BriefingSection(
|
|
302
|
+
id="project-context",
|
|
303
|
+
title="Project context",
|
|
304
|
+
applies=_project_context_applies,
|
|
305
|
+
render=_render_project_context,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _skills_applies(ctx: BriefingContext) -> bool:
|
|
310
|
+
return len(ctx.skills or ()) > 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _render_skills(ctx: BriefingContext) -> str:
|
|
314
|
+
entries = []
|
|
315
|
+
for card in ctx.skills or ():
|
|
316
|
+
entries.append(
|
|
317
|
+
"\n".join(
|
|
318
|
+
[
|
|
319
|
+
" <skill>",
|
|
320
|
+
f" <name>{_escape_xml(card.name)}</name>",
|
|
321
|
+
f" <description>{_escape_xml(card.description)}</description>",
|
|
322
|
+
f" <location>{_escape_xml(card.location)}</location>",
|
|
323
|
+
" </skill>",
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
joined = "\n".join(entries)
|
|
328
|
+
return _join_blocks(
|
|
329
|
+
[
|
|
330
|
+
"# Skills",
|
|
331
|
+
"Each skill below is a set of task-specific instructions stored on disk. When a task lines up with a skill's description, load its file with the reader and follow it. Paths a skill mentions are relative to that skill's own directory.",
|
|
332
|
+
f"<available_skills>\n{joined}\n</available_skills>",
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
#: Skills block. Present when the context carries model-invocable cards.
|
|
338
|
+
#: Renders the cards into an ``<available_skills>`` block (the format's shape)
|
|
339
|
+
#: with a re-authored lead-in.
|
|
340
|
+
SKILLS_SECTION: Final[BriefingSection] = BriefingSection(
|
|
341
|
+
id="skills", title="Skills", applies=_skills_applies, render=_render_skills
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _render_footer(ctx: BriefingContext) -> str:
|
|
346
|
+
where = ctx.cwd if ctx.cwd is not None else ctx.workspace
|
|
347
|
+
when = _iso_utc(ctx.now if ctx.now is not None else datetime.now(timezone.utc))
|
|
348
|
+
lines: list[str] = []
|
|
349
|
+
if where:
|
|
350
|
+
lines.append(f"Working directory: {where}")
|
|
351
|
+
lines.append(f"Current time: {when}")
|
|
352
|
+
return "\n".join(lines)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
#: Footer. Always present. Stamps the working directory and the current time
|
|
356
|
+
#: so the model has its bearings.
|
|
357
|
+
FOOTER_SECTION: Final[BriefingSection] = BriefingSection(id="footer", render=_render_footer)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
#: The default briefing recipe, in render order. Swap or reorder entries to
|
|
361
|
+
#: reshape the prompt without touching :func:`compose_briefing`.
|
|
362
|
+
BRIEFING_SECTIONS: Final[Briefing] = (
|
|
363
|
+
ROLE_SECTION,
|
|
364
|
+
TOOLS_SECTION,
|
|
365
|
+
GUIDELINES_SECTION,
|
|
366
|
+
TASK_SECTION,
|
|
367
|
+
SUBAGENTS_SECTION,
|
|
368
|
+
CONNECTORS_SECTION,
|
|
369
|
+
PROJECT_CONTEXT_SECTION,
|
|
370
|
+
SKILLS_SECTION,
|
|
371
|
+
FOOTER_SECTION,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
# Composer
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def compose_briefing(value: BriefingContext | BriefingInputs) -> str:
|
|
381
|
+
"""Fold a section recipe and a context into the final briefing string.
|
|
382
|
+
|
|
383
|
+
Accepts either a plain :class:`~induscode.briefing.contract.BriefingContext`
|
|
384
|
+
(using the default :data:`BRIEFING_SECTIONS` recipe) or a full
|
|
385
|
+
:class:`~induscode.briefing.contract.BriefingInputs` bundle that names its
|
|
386
|
+
own sections and optional ``prelude`` / ``append`` text. The composer
|
|
387
|
+
keeps each section whose ``applies`` predicate is satisfied (or absent),
|
|
388
|
+
renders it, discards empty fragments, and joins the rest with blank-line
|
|
389
|
+
gaps. Optional ``prelude`` and ``append`` bracket the rendered sections.
|
|
390
|
+
|
|
391
|
+
:param value: either the render context, or a full inputs bundle
|
|
392
|
+
:returns: the assembled system-prompt string
|
|
393
|
+
"""
|
|
394
|
+
inputs = (
|
|
395
|
+
value
|
|
396
|
+
if isinstance(value, BriefingInputs)
|
|
397
|
+
else BriefingInputs(sections=BRIEFING_SECTIONS, context=value)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
rendered: list[str] = []
|
|
401
|
+
if inputs.prelude is not None and inputs.prelude.strip():
|
|
402
|
+
rendered.append(inputs.prelude.strip())
|
|
403
|
+
|
|
404
|
+
for section in inputs.sections:
|
|
405
|
+
if section.applies is not None and not section.applies(inputs.context):
|
|
406
|
+
continue
|
|
407
|
+
fragment = section.render(inputs.context)
|
|
408
|
+
if fragment.strip():
|
|
409
|
+
rendered.append(fragment.strip())
|
|
410
|
+
|
|
411
|
+
if inputs.append is not None and inputs.append.strip():
|
|
412
|
+
rendered.append(inputs.append.strip())
|
|
413
|
+
|
|
414
|
+
return _join_blocks(rendered)
|