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.
Files changed (203) hide show
  1. rlmagent/__init__.py +1 -0
  2. rlmagent/__main__.py +8 -0
  3. rlmagent/acp/__init__.py +1 -0
  4. rlmagent/acp/runtime.py +71 -0
  5. rlmagent/acp/server.py +598 -0
  6. rlmagent/acp/updates.py +128 -0
  7. rlmagent/agentnet.py +237 -0
  8. rlmagent/agents/__init__.py +1 -0
  9. rlmagent/agents/approval.py +90 -0
  10. rlmagent/agents/bridge.py +109 -0
  11. rlmagent/agents/budget.py +212 -0
  12. rlmagent/agents/child_handles.py +46 -0
  13. rlmagent/agents/child_mailbox.py +30 -0
  14. rlmagent/agents/kernel_api.py +156 -0
  15. rlmagent/agents/output.py +34 -0
  16. rlmagent/agents/patch.py +107 -0
  17. rlmagent/agents/prompts.py +156 -0
  18. rlmagent/agents/trace.py +290 -0
  19. rlmagent/agents/tree.py +682 -0
  20. rlmagent/agents/wasm_api.py +105 -0
  21. rlmagent/agents/workspace.py +357 -0
  22. rlmagent/autopilot.py +447 -0
  23. rlmagent/backend.py +946 -0
  24. rlmagent/backend_context.py +74 -0
  25. rlmagent/cli/__init__.py +5 -0
  26. rlmagent/cli/config_cmd.py +177 -0
  27. rlmagent/cli/health.py +156 -0
  28. rlmagent/cli/main.py +1194 -0
  29. rlmagent/cli/update.py +188 -0
  30. rlmagent/clipboard.py +63 -0
  31. rlmagent/config/__init__.py +37 -0
  32. rlmagent/config/config.py +297 -0
  33. rlmagent/config/loader.py +119 -0
  34. rlmagent/config/prompts.py +124 -0
  35. rlmagent/config/secure_input.py +213 -0
  36. rlmagent/config/store.py +275 -0
  37. rlmagent/context/__init__.py +47 -0
  38. rlmagent/context/budget.py +557 -0
  39. rlmagent/contract.py +311 -0
  40. rlmagent/contract_run.py +232 -0
  41. rlmagent/conversation.py +2547 -0
  42. rlmagent/cron.py +77 -0
  43. rlmagent/directives.py +893 -0
  44. rlmagent/discovery.py +356 -0
  45. rlmagent/execution_log.py +66 -0
  46. rlmagent/factory.py +189 -0
  47. rlmagent/factory_graph.py +413 -0
  48. rlmagent/harness/__init__.py +63 -0
  49. rlmagent/harness/contracts/__init__.py +1 -0
  50. rlmagent/harness/contracts/stream.py +129 -0
  51. rlmagent/harness/contracts/tooling.py +127 -0
  52. rlmagent/harness/contracts/transcript/__init__.py +68 -0
  53. rlmagent/harness/contracts/transcript/_base.py +29 -0
  54. rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
  55. rlmagent/harness/contracts/transcript/entries.py +180 -0
  56. rlmagent/harness/contracts/transcript/segments.py +64 -0
  57. rlmagent/harness/contracts/values.py +6 -0
  58. rlmagent/harness/driver.py +397 -0
  59. rlmagent/harness/engine.py +489 -0
  60. rlmagent/harness/mending.py +153 -0
  61. rlmagent/harness/provider/__init__.py +1 -0
  62. rlmagent/harness/provider/base.py +27 -0
  63. rlmagent/harness/provider/wire.py +189 -0
  64. rlmagent/harness/py.typed +0 -0
  65. rlmagent/harness/session/__init__.py +5 -0
  66. rlmagent/harness/session/index.py +218 -0
  67. rlmagent/harness/session/lease.py +66 -0
  68. rlmagent/harness/session/records.py +118 -0
  69. rlmagent/harness/session/replay.py +165 -0
  70. rlmagent/harness/session/store.py +155 -0
  71. rlmagent/harness/session/summary.py +46 -0
  72. rlmagent/hints.py +47 -0
  73. rlmagent/history.py +92 -0
  74. rlmagent/hooks/__init__.py +1 -0
  75. rlmagent/hooks/hooks.py +1 -0
  76. rlmagent/instructions.py +157 -0
  77. rlmagent/job_runner.py +117 -0
  78. rlmagent/jobs.py +365 -0
  79. rlmagent/kept_agents.py +397 -0
  80. rlmagent/kernel/__init__.py +5 -0
  81. rlmagent/kernel/session.py +205 -0
  82. rlmagent/kernel/wasm.py +169 -0
  83. rlmagent/logs.py +208 -0
  84. rlmagent/managed_release.py +368 -0
  85. rlmagent/mcp.py +411 -0
  86. rlmagent/mcp_oauth.py +213 -0
  87. rlmagent/model/__init__.py +29 -0
  88. rlmagent/model/_claude/__init__.py +1 -0
  89. rlmagent/model/_claude/courier.py +117 -0
  90. rlmagent/model/_claude/emitter.py +378 -0
  91. rlmagent/model/_oai/__init__.py +1 -0
  92. rlmagent/model/_oai/helpers.py +256 -0
  93. rlmagent/model/_oai/normalize.py +263 -0
  94. rlmagent/model/_oai/parsers.py +317 -0
  95. rlmagent/model/_oai/payloads.py +226 -0
  96. rlmagent/model/_oai/transport.py +127 -0
  97. rlmagent/model/claude.py +552 -0
  98. rlmagent/model/copilot.py +142 -0
  99. rlmagent/model/correlation.py +21 -0
  100. rlmagent/model/deadline.py +19 -0
  101. rlmagent/model/discovery.py +181 -0
  102. rlmagent/model/external/__init__.py +11 -0
  103. rlmagent/model/external/acp.py +202 -0
  104. rlmagent/model/external/acp_install.py +109 -0
  105. rlmagent/model/external/cli.py +248 -0
  106. rlmagent/model/external/textcalls.py +57 -0
  107. rlmagent/model/failover.py +129 -0
  108. rlmagent/model/google.py +199 -0
  109. rlmagent/model/limits.py +46 -0
  110. rlmagent/model/oai_compatible.py +414 -0
  111. rlmagent/model/preferences.py +56 -0
  112. rlmagent/model/py.typed +0 -0
  113. rlmagent/model/registered_oauth.py +195 -0
  114. rlmagent/model/scripted.py +55 -0
  115. rlmagent/model/settings.py +271 -0
  116. rlmagent/model/subscription.py +398 -0
  117. rlmagent/model/token_bounds.py +15 -0
  118. rlmagent/model/transport/__init__.py +1 -0
  119. rlmagent/model/transport/backoff.py +87 -0
  120. rlmagent/model/transport/client.py +87 -0
  121. rlmagent/model/transport/faults.py +65 -0
  122. rlmagent/model/transport/responses_socket.py +223 -0
  123. rlmagent/model/vertex.py +93 -0
  124. rlmagent/network.py +123 -0
  125. rlmagent/packages.py +246 -0
  126. rlmagent/planning.py +84 -0
  127. rlmagent/playbook/__init__.py +1 -0
  128. rlmagent/playbook/apply.py +106 -0
  129. rlmagent/playbook/digest.py +63 -0
  130. rlmagent/playbook/improver.py +272 -0
  131. rlmagent/playbook/plan.py +99 -0
  132. rlmagent/playbook/store.py +67 -0
  133. rlmagent/plugins.py +327 -0
  134. rlmagent/policy.py +105 -0
  135. rlmagent/prompt_refs.py +57 -0
  136. rlmagent/prompts.py +331 -0
  137. rlmagent/prune.py +60 -0
  138. rlmagent/py.typed +0 -0
  139. rlmagent/reasoning.py +135 -0
  140. rlmagent/refinement/__init__.py +1 -0
  141. rlmagent/refinement/controller.py +448 -0
  142. rlmagent/refinement/engine.py +220 -0
  143. rlmagent/refinement/evidence.py +131 -0
  144. rlmagent/refinement/scoped.py +90 -0
  145. rlmagent/refinement/store.py +193 -0
  146. rlmagent/refresh.py +202 -0
  147. rlmagent/routing.py +52 -0
  148. rlmagent/runtime.py +575 -0
  149. rlmagent/safety.py +267 -0
  150. rlmagent/session_html.py +57 -0
  151. rlmagent/session_import.py +52 -0
  152. rlmagent/session_share.py +55 -0
  153. rlmagent/sessions.py +249 -0
  154. rlmagent/shell_sandbox.py +88 -0
  155. rlmagent/skillset.py +320 -0
  156. rlmagent/steering.py +148 -0
  157. rlmagent/stops.py +62 -0
  158. rlmagent/subscriptions.py +219 -0
  159. rlmagent/tasks.py +98 -0
  160. rlmagent/toolhost.py +310 -0
  161. rlmagent/tools/__init__.py +56 -0
  162. rlmagent/tools/_shared.py +210 -0
  163. rlmagent/tools/files.py +462 -0
  164. rlmagent/tools/kernel.py +93 -0
  165. rlmagent/tools/patch_tool.py +68 -0
  166. rlmagent/tools/search.py +188 -0
  167. rlmagent/trust.py +383 -0
  168. rlmagent/tui/__init__.py +1 -0
  169. rlmagent/tui/adapter.py +506 -0
  170. rlmagent/tui/app.py +711 -0
  171. rlmagent/tui/approval_screen.py +57 -0
  172. rlmagent/tui/choice_screen.py +47 -0
  173. rlmagent/tui/completions.py +116 -0
  174. rlmagent/tui/components.py +25 -0
  175. rlmagent/tui/composer.py +40 -0
  176. rlmagent/tui/factory_view.py +86 -0
  177. rlmagent/tui/inspect.py +193 -0
  178. rlmagent/tui/messages.py +215 -0
  179. rlmagent/tui/model_screen.py +307 -0
  180. rlmagent/tui/prompt.py +98 -0
  181. rlmagent/tui/question_screen.py +52 -0
  182. rlmagent/tui/replay.py +141 -0
  183. rlmagent/tui/rlm_agent.tcss +405 -0
  184. rlmagent/tui/search_box.py +91 -0
  185. rlmagent/tui/sidebar.py +182 -0
  186. rlmagent/tui/status.py +49 -0
  187. rlmagent/tui/steps.py +283 -0
  188. rlmagent/tui/suggest.py +778 -0
  189. rlmagent/tui/transcript.py +172 -0
  190. rlmagent/tui/trust_dialog.py +58 -0
  191. rlmagent/tui/worker_attach.py +141 -0
  192. rlmagent/ui/__init__.py +1 -0
  193. rlmagent/ui/render.py +211 -0
  194. rlmagent/undo.py +109 -0
  195. rlmagent/utils/__init__.py +1 -0
  196. rlmagent/websearch.py +75 -0
  197. rlmagent/worker.py +1275 -0
  198. rlmagent/worker_service.py +203 -0
  199. rlmness_cli-0.2.0.dist-info/METADATA +882 -0
  200. rlmness_cli-0.2.0.dist-info/RECORD +203 -0
  201. rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
  202. rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
  203. 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."""