rlmness-cli 0.2.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.
- rlmagent/__init__.py +1 -0
- rlmagent/__main__.py +8 -0
- rlmagent/acp/__init__.py +1 -0
- rlmagent/acp/runtime.py +71 -0
- rlmagent/acp/server.py +598 -0
- rlmagent/acp/updates.py +128 -0
- rlmagent/agentnet.py +237 -0
- rlmagent/agents/__init__.py +1 -0
- rlmagent/agents/approval.py +90 -0
- rlmagent/agents/bridge.py +109 -0
- rlmagent/agents/budget.py +212 -0
- rlmagent/agents/child_handles.py +46 -0
- rlmagent/agents/child_mailbox.py +30 -0
- rlmagent/agents/kernel_api.py +156 -0
- rlmagent/agents/output.py +34 -0
- rlmagent/agents/patch.py +107 -0
- rlmagent/agents/prompts.py +156 -0
- rlmagent/agents/trace.py +290 -0
- rlmagent/agents/tree.py +682 -0
- rlmagent/agents/wasm_api.py +105 -0
- rlmagent/agents/workspace.py +357 -0
- rlmagent/autopilot.py +447 -0
- rlmagent/backend.py +946 -0
- rlmagent/backend_context.py +74 -0
- rlmagent/cli/__init__.py +5 -0
- rlmagent/cli/config_cmd.py +177 -0
- rlmagent/cli/health.py +156 -0
- rlmagent/cli/main.py +1194 -0
- rlmagent/cli/update.py +188 -0
- rlmagent/clipboard.py +63 -0
- rlmagent/config/__init__.py +37 -0
- rlmagent/config/config.py +297 -0
- rlmagent/config/loader.py +119 -0
- rlmagent/config/prompts.py +124 -0
- rlmagent/config/secure_input.py +213 -0
- rlmagent/config/store.py +275 -0
- rlmagent/context/__init__.py +47 -0
- rlmagent/context/budget.py +557 -0
- rlmagent/contract.py +311 -0
- rlmagent/contract_run.py +232 -0
- rlmagent/conversation.py +2547 -0
- rlmagent/cron.py +77 -0
- rlmagent/directives.py +893 -0
- rlmagent/discovery.py +356 -0
- rlmagent/execution_log.py +66 -0
- rlmagent/factory.py +189 -0
- rlmagent/factory_graph.py +413 -0
- rlmagent/harness/__init__.py +63 -0
- rlmagent/harness/contracts/__init__.py +1 -0
- rlmagent/harness/contracts/stream.py +129 -0
- rlmagent/harness/contracts/tooling.py +127 -0
- rlmagent/harness/contracts/transcript/__init__.py +68 -0
- rlmagent/harness/contracts/transcript/_base.py +29 -0
- rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
- rlmagent/harness/contracts/transcript/entries.py +180 -0
- rlmagent/harness/contracts/transcript/segments.py +64 -0
- rlmagent/harness/contracts/values.py +6 -0
- rlmagent/harness/driver.py +397 -0
- rlmagent/harness/engine.py +489 -0
- rlmagent/harness/mending.py +153 -0
- rlmagent/harness/provider/__init__.py +1 -0
- rlmagent/harness/provider/base.py +27 -0
- rlmagent/harness/provider/wire.py +189 -0
- rlmagent/harness/py.typed +0 -0
- rlmagent/harness/session/__init__.py +5 -0
- rlmagent/harness/session/index.py +218 -0
- rlmagent/harness/session/lease.py +66 -0
- rlmagent/harness/session/records.py +118 -0
- rlmagent/harness/session/replay.py +165 -0
- rlmagent/harness/session/store.py +155 -0
- rlmagent/harness/session/summary.py +46 -0
- rlmagent/hints.py +47 -0
- rlmagent/history.py +92 -0
- rlmagent/hooks/__init__.py +1 -0
- rlmagent/hooks/hooks.py +1 -0
- rlmagent/instructions.py +157 -0
- rlmagent/job_runner.py +117 -0
- rlmagent/jobs.py +365 -0
- rlmagent/kept_agents.py +397 -0
- rlmagent/kernel/__init__.py +5 -0
- rlmagent/kernel/session.py +205 -0
- rlmagent/kernel/wasm.py +169 -0
- rlmagent/logs.py +208 -0
- rlmagent/managed_release.py +368 -0
- rlmagent/mcp.py +411 -0
- rlmagent/mcp_oauth.py +213 -0
- rlmagent/model/__init__.py +29 -0
- rlmagent/model/_claude/__init__.py +1 -0
- rlmagent/model/_claude/courier.py +117 -0
- rlmagent/model/_claude/emitter.py +378 -0
- rlmagent/model/_oai/__init__.py +1 -0
- rlmagent/model/_oai/helpers.py +256 -0
- rlmagent/model/_oai/normalize.py +263 -0
- rlmagent/model/_oai/parsers.py +317 -0
- rlmagent/model/_oai/payloads.py +226 -0
- rlmagent/model/_oai/transport.py +127 -0
- rlmagent/model/claude.py +552 -0
- rlmagent/model/copilot.py +142 -0
- rlmagent/model/correlation.py +21 -0
- rlmagent/model/deadline.py +19 -0
- rlmagent/model/discovery.py +181 -0
- rlmagent/model/external/__init__.py +11 -0
- rlmagent/model/external/acp.py +202 -0
- rlmagent/model/external/acp_install.py +109 -0
- rlmagent/model/external/cli.py +248 -0
- rlmagent/model/external/textcalls.py +57 -0
- rlmagent/model/failover.py +129 -0
- rlmagent/model/google.py +199 -0
- rlmagent/model/limits.py +46 -0
- rlmagent/model/oai_compatible.py +414 -0
- rlmagent/model/preferences.py +56 -0
- rlmagent/model/py.typed +0 -0
- rlmagent/model/registered_oauth.py +195 -0
- rlmagent/model/scripted.py +55 -0
- rlmagent/model/settings.py +271 -0
- rlmagent/model/subscription.py +398 -0
- rlmagent/model/token_bounds.py +15 -0
- rlmagent/model/transport/__init__.py +1 -0
- rlmagent/model/transport/backoff.py +87 -0
- rlmagent/model/transport/client.py +87 -0
- rlmagent/model/transport/faults.py +65 -0
- rlmagent/model/transport/responses_socket.py +223 -0
- rlmagent/model/vertex.py +93 -0
- rlmagent/network.py +123 -0
- rlmagent/packages.py +246 -0
- rlmagent/planning.py +84 -0
- rlmagent/playbook/__init__.py +1 -0
- rlmagent/playbook/apply.py +106 -0
- rlmagent/playbook/digest.py +63 -0
- rlmagent/playbook/improver.py +272 -0
- rlmagent/playbook/plan.py +99 -0
- rlmagent/playbook/store.py +67 -0
- rlmagent/plugins.py +327 -0
- rlmagent/policy.py +105 -0
- rlmagent/prompt_refs.py +57 -0
- rlmagent/prompts.py +331 -0
- rlmagent/prune.py +60 -0
- rlmagent/py.typed +0 -0
- rlmagent/reasoning.py +135 -0
- rlmagent/refinement/__init__.py +1 -0
- rlmagent/refinement/controller.py +448 -0
- rlmagent/refinement/engine.py +220 -0
- rlmagent/refinement/evidence.py +131 -0
- rlmagent/refinement/scoped.py +90 -0
- rlmagent/refinement/store.py +193 -0
- rlmagent/refresh.py +202 -0
- rlmagent/routing.py +52 -0
- rlmagent/runtime.py +575 -0
- rlmagent/safety.py +267 -0
- rlmagent/session_html.py +57 -0
- rlmagent/session_import.py +52 -0
- rlmagent/session_share.py +55 -0
- rlmagent/sessions.py +249 -0
- rlmagent/shell_sandbox.py +88 -0
- rlmagent/skillset.py +320 -0
- rlmagent/steering.py +148 -0
- rlmagent/stops.py +62 -0
- rlmagent/subscriptions.py +219 -0
- rlmagent/tasks.py +98 -0
- rlmagent/toolhost.py +310 -0
- rlmagent/tools/__init__.py +56 -0
- rlmagent/tools/_shared.py +210 -0
- rlmagent/tools/files.py +462 -0
- rlmagent/tools/kernel.py +93 -0
- rlmagent/tools/patch_tool.py +68 -0
- rlmagent/tools/search.py +188 -0
- rlmagent/trust.py +383 -0
- rlmagent/tui/__init__.py +1 -0
- rlmagent/tui/adapter.py +506 -0
- rlmagent/tui/app.py +711 -0
- rlmagent/tui/approval_screen.py +57 -0
- rlmagent/tui/choice_screen.py +47 -0
- rlmagent/tui/completions.py +116 -0
- rlmagent/tui/components.py +25 -0
- rlmagent/tui/composer.py +40 -0
- rlmagent/tui/factory_view.py +86 -0
- rlmagent/tui/inspect.py +193 -0
- rlmagent/tui/messages.py +215 -0
- rlmagent/tui/model_screen.py +307 -0
- rlmagent/tui/prompt.py +98 -0
- rlmagent/tui/question_screen.py +52 -0
- rlmagent/tui/replay.py +141 -0
- rlmagent/tui/rlm_agent.tcss +405 -0
- rlmagent/tui/search_box.py +91 -0
- rlmagent/tui/sidebar.py +182 -0
- rlmagent/tui/status.py +49 -0
- rlmagent/tui/steps.py +283 -0
- rlmagent/tui/suggest.py +778 -0
- rlmagent/tui/transcript.py +172 -0
- rlmagent/tui/trust_dialog.py +58 -0
- rlmagent/tui/worker_attach.py +141 -0
- rlmagent/ui/__init__.py +1 -0
- rlmagent/ui/render.py +211 -0
- rlmagent/undo.py +109 -0
- rlmagent/utils/__init__.py +1 -0
- rlmagent/websearch.py +75 -0
- rlmagent/worker.py +1275 -0
- rlmagent/worker_service.py +203 -0
- rlmness_cli-0.2.0.dist-info/METADATA +882 -0
- rlmness_cli-0.2.0.dist-info/RECORD +203 -0
- rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
- rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
rlmagent/prompts.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Markdown prompt template system: discovery, loading, rendering, and slash-command expansion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# ── constants ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
_MD_SUFFIX = ".md"
|
|
14
|
+
_FRONT_MATTER_RE = re.compile(r"\A---[ \t]*\n(.*?\n)---[ \t]*\n", re.DOTALL)
|
|
15
|
+
_PLACEHOLDER_RE = re.compile(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")
|
|
16
|
+
_RESERVED_PREFIXES = ("skill:",)
|
|
17
|
+
_ARGUMENT_ALIASES = frozenset({"arguments", "args"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── diagnostic severity ───────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Severity(Enum):
|
|
24
|
+
"""Non-fatal diagnostic severity level."""
|
|
25
|
+
|
|
26
|
+
INFO = "info"
|
|
27
|
+
WARNING = "warning"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── data models ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class PromptTemplate:
|
|
35
|
+
"""An immutable, loaded markdown prompt template."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
path: Path
|
|
39
|
+
content: str
|
|
40
|
+
description: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class TemplateDiagnostic:
|
|
45
|
+
"""A non-fatal issue encountered during template discovery."""
|
|
46
|
+
|
|
47
|
+
severity: Severity
|
|
48
|
+
path: Path
|
|
49
|
+
message: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── errors ─────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TemplateLoadError(Exception):
|
|
56
|
+
"""Fatal error while loading a template file."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TemplateRenderError(Exception):
|
|
60
|
+
"""A required placeholder variable was not supplied."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── front-matter helpers ──────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_front_matter(text: str) -> tuple[dict[str, str], str]:
|
|
67
|
+
"""Split optional YAML front matter from the markdown body."""
|
|
68
|
+
match = _FRONT_MATTER_RE.match(text)
|
|
69
|
+
if match is None:
|
|
70
|
+
return {}, text
|
|
71
|
+
|
|
72
|
+
raw = match.group(1)
|
|
73
|
+
body = text[match.end():]
|
|
74
|
+
meta: dict[str, str] = {}
|
|
75
|
+
for line in raw.splitlines():
|
|
76
|
+
stripped = line.strip()
|
|
77
|
+
if not stripped or stripped.startswith("#"):
|
|
78
|
+
continue
|
|
79
|
+
colon = stripped.find(":")
|
|
80
|
+
if colon < 1:
|
|
81
|
+
continue
|
|
82
|
+
key = stripped[:colon].strip()
|
|
83
|
+
value = stripped[colon + 1:].strip()
|
|
84
|
+
if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
|
|
85
|
+
value = value[1:-1]
|
|
86
|
+
meta[key] = value
|
|
87
|
+
return meta, body
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _derive_description(body: str) -> str | None:
|
|
91
|
+
"""Extract the first non-heading, non-blank line as a description."""
|
|
92
|
+
for line in body.splitlines():
|
|
93
|
+
stripped = line.strip()
|
|
94
|
+
if not stripped:
|
|
95
|
+
continue
|
|
96
|
+
if stripped.startswith("#"):
|
|
97
|
+
continue
|
|
98
|
+
if stripped.startswith("---"):
|
|
99
|
+
continue
|
|
100
|
+
return stripped
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── single-file loading ───────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _template_name_from_path(path: Path) -> str:
|
|
108
|
+
"""Derive a template name from a file path (stem without extension)."""
|
|
109
|
+
return path.stem
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _load_template_file(path: Path) -> PromptTemplate:
|
|
113
|
+
"""Read a single markdown file and return a ``PromptTemplate``."""
|
|
114
|
+
try:
|
|
115
|
+
raw = path.read_text(encoding="utf-8")
|
|
116
|
+
except OSError as exc:
|
|
117
|
+
raise TemplateLoadError(f"Cannot read {path}: {exc}") from exc
|
|
118
|
+
except UnicodeDecodeError as exc:
|
|
119
|
+
raise TemplateLoadError(f"{path} is not valid UTF-8: {exc}") from exc
|
|
120
|
+
|
|
121
|
+
meta, body = _parse_front_matter(raw)
|
|
122
|
+
name = meta.get("name", _template_name_from_path(path))
|
|
123
|
+
description = meta.get("description") or _derive_description(body)
|
|
124
|
+
|
|
125
|
+
return PromptTemplate(
|
|
126
|
+
name=name,
|
|
127
|
+
path=path,
|
|
128
|
+
content=body,
|
|
129
|
+
description=description,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── directory discovery ───────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _discover_in_directory(
|
|
137
|
+
root: Path,
|
|
138
|
+
) -> tuple[list[PromptTemplate], list[TemplateDiagnostic]]:
|
|
139
|
+
"""Scan *root* for ``.md`` files and load them as templates."""
|
|
140
|
+
templates: list[PromptTemplate] = []
|
|
141
|
+
diagnostics: list[TemplateDiagnostic] = []
|
|
142
|
+
seen: dict[str, Path] = {}
|
|
143
|
+
|
|
144
|
+
if not root.is_dir():
|
|
145
|
+
return templates, diagnostics
|
|
146
|
+
|
|
147
|
+
for child in sorted(root.iterdir()):
|
|
148
|
+
if child.is_dir():
|
|
149
|
+
continue
|
|
150
|
+
if child.suffix.lower() != _MD_SUFFIX:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
tpl = _load_template_file(child)
|
|
155
|
+
except TemplateLoadError as exc:
|
|
156
|
+
diagnostics.append(TemplateDiagnostic(
|
|
157
|
+
severity=Severity.WARNING,
|
|
158
|
+
path=child,
|
|
159
|
+
message=str(exc),
|
|
160
|
+
))
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
if tpl.name in seen:
|
|
164
|
+
diagnostics.append(TemplateDiagnostic(
|
|
165
|
+
severity=Severity.WARNING,
|
|
166
|
+
path=child,
|
|
167
|
+
message=(
|
|
168
|
+
f"Duplicate template name {tpl.name!r}; "
|
|
169
|
+
f"already loaded from {seen[tpl.name]}"
|
|
170
|
+
),
|
|
171
|
+
))
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
seen[tpl.name] = child
|
|
175
|
+
templates.append(tpl)
|
|
176
|
+
|
|
177
|
+
return templates, diagnostics
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── public loading API ─────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_prompt_templates_with_diagnostics(
|
|
184
|
+
resource_dirs: Sequence[Path],
|
|
185
|
+
) -> tuple[list[PromptTemplate], list[TemplateDiagnostic]]:
|
|
186
|
+
"""Load templates from multiple resource directories with diagnostics."""
|
|
187
|
+
combined: dict[str, PromptTemplate] = {}
|
|
188
|
+
all_diagnostics: list[TemplateDiagnostic] = []
|
|
189
|
+
|
|
190
|
+
for root in resource_dirs:
|
|
191
|
+
templates, diags = _discover_in_directory(root)
|
|
192
|
+
all_diagnostics.extend(diags)
|
|
193
|
+
for tpl in templates:
|
|
194
|
+
if tpl.name not in combined:
|
|
195
|
+
combined[tpl.name] = tpl
|
|
196
|
+
|
|
197
|
+
return list(combined.values()), all_diagnostics
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_prompt_templates(resource_dirs: Sequence[Path]) -> list[PromptTemplate]:
|
|
201
|
+
"""Load templates from resource directories, discarding diagnostics."""
|
|
202
|
+
templates, _ = load_prompt_templates_with_diagnostics(resource_dirs)
|
|
203
|
+
return templates
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── placeholder utilities ─────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _collect_placeholders(content: str) -> set[str]:
|
|
210
|
+
"""Return the set of placeholder names found in *content*."""
|
|
211
|
+
return set(_PLACEHOLDER_RE.findall(content))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _references_arguments(content: str) -> bool:
|
|
215
|
+
"""Return whether *content* contains an ``{{arguments}}`` or ``{{args}}`` placeholder."""
|
|
216
|
+
return bool(_ARGUMENT_ALIASES & _collect_placeholders(content))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── rendering ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def render_template(
|
|
223
|
+
template: PromptTemplate,
|
|
224
|
+
variables: Mapping[str, str] | None = None,
|
|
225
|
+
*,
|
|
226
|
+
strict: bool = True,
|
|
227
|
+
missing_value: str = "",
|
|
228
|
+
) -> str:
|
|
229
|
+
"""Render a template by replacing ``{{ var }}`` placeholders."""
|
|
230
|
+
vars_ = variables or {}
|
|
231
|
+
|
|
232
|
+
def _replace(match: re.Match[str]) -> str:
|
|
233
|
+
name = match.group(1)
|
|
234
|
+
if name in vars_:
|
|
235
|
+
return vars_[name]
|
|
236
|
+
if strict:
|
|
237
|
+
raise TemplateRenderError(
|
|
238
|
+
f"Missing variable {name!r} in template {template.name!r}"
|
|
239
|
+
)
|
|
240
|
+
return missing_value
|
|
241
|
+
|
|
242
|
+
return _PLACEHOLDER_RE.sub(_replace, template.content)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── slash-command parsing ──────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _is_reserved_command(name: str) -> bool:
|
|
249
|
+
"""Return whether *name* belongs to another command system."""
|
|
250
|
+
return any(name.startswith(prefix) for prefix in _RESERVED_PREFIXES)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _parse_slash_command(text: str) -> tuple[str, str] | None:
|
|
254
|
+
"""Parse ``/<name> args…`` into ``(name, args)`` or return ``None``."""
|
|
255
|
+
if not text.startswith("/"):
|
|
256
|
+
return None
|
|
257
|
+
if text.startswith("//"):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
body = text[1:]
|
|
261
|
+
parts = body.split(None, 1)
|
|
262
|
+
if not parts:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
name = parts[0]
|
|
266
|
+
if _is_reserved_command(name):
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
arguments = parts[1] if len(parts) > 1 else ""
|
|
270
|
+
return name, arguments
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _lookup_template(
|
|
274
|
+
name: str,
|
|
275
|
+
registry: Mapping[str, PromptTemplate],
|
|
276
|
+
) -> PromptTemplate | None:
|
|
277
|
+
"""Find a template by name, returning ``None`` on miss."""
|
|
278
|
+
return registry.get(name)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_variables(arguments: str) -> dict[str, str]:
|
|
282
|
+
"""Build the default variable mapping for a slash-command invocation."""
|
|
283
|
+
return {"arguments": arguments, "args": arguments}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _maybe_append_arguments(rendered: str, arguments: str, content: str) -> str:
|
|
287
|
+
"""Append *arguments* to *rendered* if the template never referenced them."""
|
|
288
|
+
if not arguments:
|
|
289
|
+
return rendered
|
|
290
|
+
if _references_arguments(content):
|
|
291
|
+
return rendered
|
|
292
|
+
return rendered + "\n\n" + arguments
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ── public expansion API ───────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def expand_slash_command(
|
|
299
|
+
text: str,
|
|
300
|
+
registry: Mapping[str, PromptTemplate],
|
|
301
|
+
*,
|
|
302
|
+
extra_variables: Mapping[str, str] | None = None,
|
|
303
|
+
) -> str | None:
|
|
304
|
+
"""Expand a ``/<template-name> args…`` slash command."""
|
|
305
|
+
parsed = _parse_slash_command(text)
|
|
306
|
+
if parsed is None:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
name, arguments = parsed
|
|
310
|
+
tpl = _lookup_template(name, registry)
|
|
311
|
+
if tpl is None:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
variables = dict(extra_variables) if extra_variables else {}
|
|
315
|
+
variables.update(_build_variables(arguments))
|
|
316
|
+
|
|
317
|
+
rendered = render_template(tpl, variables, strict=False, missing_value="")
|
|
318
|
+
return _maybe_append_arguments(rendered, arguments, tpl.content)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
__all__ = [
|
|
322
|
+
"PromptTemplate",
|
|
323
|
+
"Severity",
|
|
324
|
+
"TemplateDiagnostic",
|
|
325
|
+
"TemplateLoadError",
|
|
326
|
+
"TemplateRenderError",
|
|
327
|
+
"expand_slash_command",
|
|
328
|
+
"load_prompt_templates",
|
|
329
|
+
"load_prompt_templates_with_diagnostics",
|
|
330
|
+
"render_template",
|
|
331
|
+
]
|
rlmagent/prune.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""`rlm-agent session prune`: find and remove old sessions and their undo snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from time import time
|
|
12
|
+
|
|
13
|
+
from rlmagent.harness.session.index import SessionCatalog
|
|
14
|
+
from rlmagent.undo import Shadow
|
|
15
|
+
|
|
16
|
+
_SAFE_ID = re.compile(r"[\w-]+")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _size(path: Path) -> int:
|
|
20
|
+
if path.is_file():
|
|
21
|
+
return path.stat().st_size
|
|
22
|
+
return sum(p.stat().st_size for p in path.rglob("*") if p.is_file())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _remove(path: Path) -> None:
|
|
26
|
+
if path.is_file():
|
|
27
|
+
path.unlink()
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
def writable(function, name, _error): # git marks its objects read-only on Windows
|
|
31
|
+
os.chmod(name, stat.S_IWRITE)
|
|
32
|
+
function(name)
|
|
33
|
+
|
|
34
|
+
shutil.rmtree(path, onexc=writable)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def prune(base: Path, home: Path, days: int, *, apply: bool = False, out=sys.stdout) -> int:
|
|
38
|
+
"""List what is older than `days`; removes it only when `apply` is set."""
|
|
39
|
+
catalog = SessionCatalog(base)
|
|
40
|
+
cutoff = time() - days * 86400
|
|
41
|
+
metas = catalog.list_all()
|
|
42
|
+
old = [m for m in metas if m.updated_at < cutoff and _SAFE_ID.fullmatch(m.session_id)]
|
|
43
|
+
kept = {Shadow(m.cwd, home).git_dir for m in metas if m not in old and m.cwd}
|
|
44
|
+
targets = [f for m in old for f in base.glob(f"{m.session_id}.*")]
|
|
45
|
+
shadows = {Shadow(m.cwd, home).git_dir for m in old if m.cwd} - kept
|
|
46
|
+
targets += [s for s in shadows if s.exists()]
|
|
47
|
+
total = sum(_size(t) for t in targets)
|
|
48
|
+
print(f"{len(old)} of {len(metas)} sessions are older than {days} days; "
|
|
49
|
+
f"{total / 1_000_000:.1f} MB in {len(targets)} files and snapshot folders.", file=out)
|
|
50
|
+
if not apply:
|
|
51
|
+
for target in targets[:20]:
|
|
52
|
+
print(f" {target}", file=out)
|
|
53
|
+
if old:
|
|
54
|
+
print("Nothing was removed. Add --yes to remove these.", file=out)
|
|
55
|
+
return 0
|
|
56
|
+
for target in targets:
|
|
57
|
+
_remove(target)
|
|
58
|
+
catalog.remove({m.session_id for m in old})
|
|
59
|
+
print("Removed.", file=out)
|
|
60
|
+
return 0
|
rlmagent/py.typed
ADDED
|
File without changes
|
rlmagent/reasoning.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Provider-neutral thinking mode primitives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
# ── canonical levels ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
ThinkingLevel = Literal["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
11
|
+
|
|
12
|
+
THINKING_LEVELS: tuple[ThinkingLevel, ...] = (
|
|
13
|
+
"off",
|
|
14
|
+
"minimal",
|
|
15
|
+
"low",
|
|
16
|
+
"medium",
|
|
17
|
+
"high",
|
|
18
|
+
"xhigh",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium"
|
|
22
|
+
|
|
23
|
+
THINKING_DESCRIPTIONS: dict[ThinkingLevel, str] = {
|
|
24
|
+
"off": "Thinking off",
|
|
25
|
+
"minimal": "Think a little",
|
|
26
|
+
"low": "Think briefly",
|
|
27
|
+
"medium": "Think normally",
|
|
28
|
+
"high": "Think hard",
|
|
29
|
+
"xhigh": "Think as long as allowed",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_LEVEL_SET: frozenset[str] = frozenset(THINKING_LEVELS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── validation ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_thinking_level(value: str | None) -> ThinkingLevel:
|
|
39
|
+
"""Resolve *value* to a canonical thinking level."""
|
|
40
|
+
if value is None:
|
|
41
|
+
return DEFAULT_THINKING_LEVEL
|
|
42
|
+
|
|
43
|
+
cleaned = value.strip().lower()
|
|
44
|
+
if cleaned not in _LEVEL_SET:
|
|
45
|
+
available = ", ".join(THINKING_LEVELS)
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Unknown thinking level {value!r}. "
|
|
48
|
+
f"Available levels: {available}"
|
|
49
|
+
)
|
|
50
|
+
return cleaned # type: ignore[return-value]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_thinking_levels(values: Sequence[str]) -> tuple[ThinkingLevel, ...]:
|
|
54
|
+
"""Normalize a sequence of thinking-level strings."""
|
|
55
|
+
if not values:
|
|
56
|
+
raise ValueError("At least one thinking level is required")
|
|
57
|
+
|
|
58
|
+
result: list[ThinkingLevel] = []
|
|
59
|
+
seen: set[ThinkingLevel] = set()
|
|
60
|
+
for v in values:
|
|
61
|
+
level = normalize_thinking_level(v)
|
|
62
|
+
if level in seen:
|
|
63
|
+
raise ValueError(f"Duplicate thinking level: {level!r}")
|
|
64
|
+
seen.add(level)
|
|
65
|
+
result.append(level)
|
|
66
|
+
return tuple(result)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── provider mappings ───────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
# Each helper converts a ThinkingLevel into the parameter shape expected
|
|
72
|
+
# by one family of model providers. Provider names are intentionally kept
|
|
73
|
+
# out of public API surface — callers pick the right helper via their own
|
|
74
|
+
# provider resolution logic.
|
|
75
|
+
|
|
76
|
+
_EFFORT_MAP: dict[ThinkingLevel, str] = {
|
|
77
|
+
"off": "none",
|
|
78
|
+
"minimal": "minimal",
|
|
79
|
+
"low": "low",
|
|
80
|
+
"medium": "medium",
|
|
81
|
+
"high": "high",
|
|
82
|
+
"xhigh": "xhigh",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_BUDGET_MAP: dict[ThinkingLevel, int | None] = {
|
|
86
|
+
"off": None,
|
|
87
|
+
"minimal": 1024,
|
|
88
|
+
"low": 2048,
|
|
89
|
+
"medium": 4096,
|
|
90
|
+
"high": 8192,
|
|
91
|
+
"xhigh": 16384,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def thinking_to_effort(level: ThinkingLevel) -> str:
|
|
96
|
+
"""Map a thinking level to an effort-style parameter value."""
|
|
97
|
+
return _EFFORT_MAP[level]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def thinking_to_budget(level: ThinkingLevel) -> int | None:
|
|
101
|
+
"""Map a thinking level to a token-budget parameter value."""
|
|
102
|
+
return _BUDGET_MAP[level]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── cycling helper ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def next_thinking_level(
|
|
109
|
+
current: str,
|
|
110
|
+
*,
|
|
111
|
+
available: Sequence[ThinkingLevel] = THINKING_LEVELS,
|
|
112
|
+
) -> ThinkingLevel:
|
|
113
|
+
"""Return the level that follows *current* in *available*."""
|
|
114
|
+
if not available:
|
|
115
|
+
return DEFAULT_THINKING_LEVEL
|
|
116
|
+
|
|
117
|
+
cleaned = current.strip().lower()
|
|
118
|
+
for i, lvl in enumerate(available):
|
|
119
|
+
if lvl == cleaned:
|
|
120
|
+
return available[(i + 1) % len(available)]
|
|
121
|
+
# Unknown value — start from the beginning.
|
|
122
|
+
return available[0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"DEFAULT_THINKING_LEVEL",
|
|
127
|
+
"THINKING_DESCRIPTIONS",
|
|
128
|
+
"THINKING_LEVELS",
|
|
129
|
+
"ThinkingLevel",
|
|
130
|
+
"next_thinking_level",
|
|
131
|
+
"normalize_thinking_level",
|
|
132
|
+
"normalize_thinking_levels",
|
|
133
|
+
"thinking_to_budget",
|
|
134
|
+
"thinking_to_effort",
|
|
135
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Explicit, bounded updates to reusable agent guidance."""
|