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/runtime.py ADDED
@@ -0,0 +1,575 @@
1
+ """Shared session construction for every rlm-agent frontend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from rlmagent.conversation import CodingSession
12
+ from rlmagent.trust import Asker, Choice, TrustOutcome, TrustRequest
13
+
14
+
15
+ async def build_session(ns: argparse.Namespace, *, session_id: str | None = None) -> CodingSession:
16
+ """Resolve configuration and return a ready ``CodingSession``."""
17
+ from uuid import uuid4
18
+
19
+ from rlmagent.cli.main import _die, _sessions_dir
20
+ from rlmagent.harness.session.lease import SessionBusy, acquire
21
+
22
+ if getattr(ns, "no_session", False):
23
+ return await _build_session(ns)
24
+ session_id = getattr(ns, "resume", None) or session_id or uuid4().hex[:12]
25
+ try:
26
+ lease = acquire(_sessions_dir(getattr(ns, "session_dir", None)), session_id)
27
+ except (SessionBusy, ValueError) as exc:
28
+ _die(str(exc))
29
+ try:
30
+ session = await _build_session(ns, session_id=session_id)
31
+ except BaseException:
32
+ lease.close()
33
+ raise
34
+ session._writer_lease = lease
35
+ return session
36
+
37
+
38
+ async def _build_session(ns: argparse.Namespace, *, session_id: str | None = None) -> CodingSession:
39
+ from rlmagent.cli.main import (
40
+ _die,
41
+ _info,
42
+ _install_hooks,
43
+ _install_safety,
44
+ _load_extensions,
45
+ _load_prompt_templates,
46
+ _load_skills,
47
+ _load_tools,
48
+ _resolve_model,
49
+ _resolve_provider,
50
+ _resolve_provider_name,
51
+ _resolve_system,
52
+ _sessions_dir,
53
+ )
54
+ from rlmagent.config.loader import apply_config, load_model
55
+ from rlmagent.conversation import CodingSession
56
+ from rlmagent.agents.prompts import root_system
57
+ from rlmagent.safety import ApprovalPolicy
58
+
59
+ # Trust first: a trusted project's settings may choose the provider.
60
+ await settle_project_trust(ns, ask=getattr(ns, "trust_ask", None))
61
+ provider_name = _resolve_provider_name(getattr(ns, "provider", None))
62
+ # Export saved credentials for this process so the provider loaders
63
+ # (which read the environment) see them. Real env vars keep precedence.
64
+ apply_config(provider_name)
65
+ model = (
66
+ getattr(ns, "model", None)
67
+ or load_model(provider_name)
68
+ or _resolve_model(None, provider_name)
69
+ )
70
+ provider = _resolve_provider(getattr(ns, "provider", None))
71
+
72
+ verbose = getattr(ns, "verbose", False)
73
+
74
+ no_session = getattr(ns, "no_session", False)
75
+ sessions_dir = None if no_session else _sessions_dir(getattr(ns, "session_dir", None))
76
+
77
+ # The root agent and every sub-agent share one tree: one budget, one
78
+ # channel back from the kernels, one place that shuts them all down.
79
+ from rlmagent.tools._shared import set_workspace
80
+
81
+ set_workspace(os.getcwd())
82
+ from rlmagent.undo import Shadow
83
+
84
+ tree = make_agent_tree(ns, provider, provider_name, model, sessions_dir, cwd=os.getcwd(),
85
+ backend=getattr(ns, "engine", "session") == "rlmness")
86
+ from rlmagent.policy import DEFAULT_OUTPUT_BYTES, Policy, git_refusal
87
+
88
+ output_limit = getattr(ns, "max_tool_output", None)
89
+ policy = tree.policy = Policy(
90
+ allow_git=not getattr(ns, "deny_git", False),
91
+ max_tool_output_bytes=DEFAULT_OUTPUT_BYTES if output_limit is None else output_limit,
92
+ max_compactions=getattr(ns, "max_compactions", None),
93
+ agent_max_tokens=getattr(ns, "agent_max_tokens", None),
94
+ search_block=list(getattr(ns, "search_block", None) or []),
95
+ shell_access=getattr(ns, "shell_access", "workspace"),
96
+ shell_network=getattr(ns, "shell_network", False),
97
+ )
98
+ await tree.start()
99
+ kernel = tree.root_kernel()
100
+ raw_provider = provider
101
+ provider = tree.provider_for("root")
102
+ if getattr(ns, "engine", "session") == "rlmness":
103
+ from rlmagent.backend import BackendProvider
104
+ from rlmagent.model.external import with_external
105
+ provider = BackendProvider(with_external(raw_provider), provider, tree)
106
+
107
+ from rlmagent.plugins import (
108
+ as_tool, contributions, load_extensions, reload_extensions, rlm_agent_home,
109
+ )
110
+ from rlmagent.tasks import TaskList
111
+
112
+ # Harness functions every agent gets: cells call them, the session engine sees tools.
113
+ tasks = TaskList()
114
+ from pathlib import Path
115
+
116
+ from rlmagent.mcp import McpHub, load_configs
117
+ from rlmagent.trust import project_inputs_allowed
118
+
119
+ here = os.getcwd()
120
+ configs, problems = load_configs(rlm_agent_home(),
121
+ Path(here) if project_inputs_allowed(here) else None)
122
+ supplied = getattr(ns, "acp_mcp", {})
123
+ if conflicts := sorted(configs.keys() & supplied.keys()):
124
+ _die("Editor MCP names already configured: " + ", ".join(conflicts))
125
+ configs.update(supplied)
126
+ hub = McpHub(configs, here, tree.approver, problems)
127
+ from rlmagent.agentnet import AgentNet
128
+
129
+ net = AgentNet("", here)
130
+ from rlmagent.autopilot import Autopilot
131
+
132
+ pilot = Autopilot(cwd=here)
133
+ from rlmagent.websearch import make_web_search
134
+
135
+ built: dict = {} # the session, once it exists; these functions run only after that
136
+
137
+ async def context_usage() -> dict:
138
+ """How full the conversation context is: used and limit in tokens, and percent."""
139
+ usage = built["session"].usage
140
+ limit = max(usage.context_limit, 1)
141
+ return {"used": usage.estimated_context_tokens, "limit": usage.context_limit,
142
+ "percent": round(100 * usage.estimated_context_tokens / limit)}
143
+
144
+ async def compact() -> str:
145
+ """Summarize older conversation when this turn ends, to free context for more work."""
146
+ built["session"].compact_requested = True
147
+ return "older conversation will be summarized when this turn ends"
148
+
149
+ from rlmagent.agents.approval import Request, check
150
+ from rlmagent.history import make_history
151
+ from rlmagent.jobs import Jobs
152
+
153
+ async def _approve_job(command: str) -> None:
154
+ request = Request("run", command)
155
+ check(await tree.approver.approve(request), request)
156
+
157
+ async def factory_run(name: str) -> dict:
158
+ """Run a named factory from the user's library after workspace approval."""
159
+ import shlex
160
+
161
+ from rlmagent.agents.workspace import Workspace
162
+ from rlmagent.factory import template
163
+
164
+ manifest = template(name)
165
+ if not manifest.is_file():
166
+ raise ValueError(f"no factory named {name!r}")
167
+ argv = [sys.executable, "-m", "rlmagent.cli.main", "factory", str(manifest)]
168
+ request = Request("run", shlex.join(argv))
169
+ check(await tree.approver.approve(request), request)
170
+ workspace = Workspace(here, tree.approver)
171
+ return await workspace._execute_argv(argv, timeout=1800)
172
+
173
+ from uuid import uuid4
174
+ session_id = getattr(ns, "resume", None) or session_id or uuid4().hex[:12]
175
+ jobs = Jobs(here, (sessions_dir or rlm_agent_home()) / "jobs" / session_id, _approve_job,
176
+ lambda command: git_refusal(command, policy),
177
+ shell_access=policy.shell_access, shell_network=policy.shell_network)
178
+ tools_at = getattr(ns, "tools_at", None)
179
+ if tools_at:
180
+ if getattr(ns, "engine", "session") != "rlmness":
181
+ _die("--tools-at needs the default engine (--engine rlmness).")
182
+ from rlmagent.toolhost import connect
183
+
184
+ try:
185
+ tree.remote_tools = await connect(tools_at)
186
+ except (OSError, ValueError, ConnectionError) as exc:
187
+ _die(f"Cannot reach the tool host at {tools_at}: {exc}")
188
+ tree.host_functions = {"todo": tasks.todo, "mcp": hub.mcp, "mcp_tools": hub.mcp_tools,
189
+ **net.functions(), **pilot.functions(),
190
+ **jobs.functions(inbox=lambda: net.take()),
191
+ "context_usage": context_usage, "compact": compact,
192
+ "factory_run": factory_run,
193
+ "web_search": make_web_search(lambda: tree.network, policy),
194
+ "history": make_history(
195
+ lambda: getattr(getattr(built.get("session"), "_vault", None),
196
+ "path", None),
197
+ lambda: getattr(getattr(built.get("session"), "backend", None),
198
+ "event_path", None))}
199
+ builtin_tools = [as_tool(name, fn) for name, fn in tree.host_functions.items()]
200
+ extended: dict = {"tools": [], "functions": {}}
201
+
202
+ def _take_extensions(result) -> None:
203
+ for failure in result.failures:
204
+ print(f"rlm-agent: extension {failure.name!r} failed: {failure.error}", file=sys.stderr)
205
+ extra, functions = contributions(result)
206
+ extended["tools"] = [*extra, *(as_tool(name, fn) for name, fn in functions.items())]
207
+ extended["functions"] = functions
208
+ tree.extension_functions = functions
209
+
210
+ _load_extensions(verbose=verbose)
211
+ _take_extensions(load_extensions())
212
+ tools = [*_load_tools(verbose=verbose, kernel=kernel), *builtin_tools, *extended["tools"]]
213
+ skills = _load_skills(os.getcwd(), verbose=verbose)
214
+ system = root_system(
215
+ _resolve_system(getattr(ns, "system_prompt", None), tools=tools, skills=skills),
216
+ can_delegate=tree.allowance.max_depth > 0,
217
+ playbook=True,
218
+ sandboxed=tree.kernel_kind == "wasm",
219
+ network=tree.network,
220
+ )
221
+
222
+ def _tools_loader():
223
+ _take_extensions(reload_extensions())
224
+ return [*_load_tools(kernel=kernel), *builtin_tools, *extended["tools"]]
225
+
226
+ def _skills_loader():
227
+ return _load_skills(os.getcwd())
228
+
229
+ def _templates_loader():
230
+ return _load_prompt_templates(os.getcwd())
231
+
232
+ resume = getattr(ns, "resume", None)
233
+ if resume:
234
+ if sessions_dir is None:
235
+ _die("Cannot use --resume together with --no-session.")
236
+ problem = await _resume_folder_problem(ns, resume, sessions_dir)
237
+ if problem:
238
+ _die(problem)
239
+ try:
240
+ session = await CodingSession.resume(
241
+ resume,
242
+ provider=provider,
243
+ provider_name=provider_name,
244
+ model=model,
245
+ system=system,
246
+ tools=tools,
247
+ sessions_dir=sessions_dir,
248
+ tools_loader=_tools_loader,
249
+ skills_loader=_skills_loader,
250
+ templates_loader=_templates_loader,
251
+ keep_model=bool(getattr(ns, "model", None)),
252
+ )
253
+ except FileNotFoundError:
254
+ _die(f"Session not found: {resume}")
255
+ if verbose:
256
+ _info(f"Resumed {len(session.transcript)} messages from {resume}")
257
+ if session.interrupted:
258
+ _info("The last turn of this session did not finish; its work may be incomplete. "
259
+ "Send a message, or /continue, to carry on.")
260
+ else:
261
+ session = await CodingSession.create(
262
+ session_id=session_id,
263
+ provider=provider,
264
+ provider_name=provider_name,
265
+ model=model,
266
+ system=system,
267
+ tools=tools,
268
+ sessions_dir=sessions_dir,
269
+ tools_loader=_tools_loader,
270
+ skills_loader=_skills_loader,
271
+ templates_loader=_templates_loader,
272
+ )
273
+
274
+ if getattr(ns, "engine", "session") == "rlmness":
275
+ from rlmness.session import Session as BackendState
276
+ provider.instructions = _resolve_system(
277
+ getattr(ns, "system_prompt", None), tools=[], skills=skills)
278
+ if sessions_dir is not None:
279
+ provider.event_path = sessions_dir / f"{session.session_id}.backend.events.jsonl"
280
+ provider.state = BackendState.load(
281
+ sessions_dir / f"{session.session_id}.backend.json")
282
+ if getattr(ns, "engine", "session") == "rlmness":
283
+ session.backend = provider
284
+ elif sessions_dir is not None:
285
+ tree.event_path = sessions_dir / f"{session.session_id}.backend.events.jsonl"
286
+ session.agent_tree = tree
287
+ session.shadow = Shadow(session.cwd, rlm_agent_home())
288
+ session.load_undo()
289
+ session.tasks = tasks
290
+ session.mcp = hub
291
+ session.agentnet = net
292
+ session.jobs = jobs
293
+ jobs.subscriptions.start()
294
+ session.remote_tools = getattr(tree, "remote_tools", None)
295
+ session.policy = policy
296
+ built["session"] = session
297
+ session.autopilot = pilot
298
+ pilot.path = sessions_dir / f"{session.session_id}.goal.json" if sessions_dir else None
299
+ pilot.used_tokens = lambda: (session.usage.total_input_tokens
300
+ + session.usage.total_output_tokens)
301
+ pilot.load()
302
+ net.me = f"s-{session.session_id[:8]}"
303
+ if sessions_dir is not None:
304
+ tree.tracer.persist(sessions_dir / f"{session.session_id}.requests.jsonl")
305
+ net.register()
306
+
307
+ def _progress(text: str) -> None:
308
+ backend = getattr(session, "backend", None)
309
+ if backend is not None and hasattr(backend, "record_event"):
310
+ backend.record_event({"kind": "progress", "text": text})
311
+
312
+ net.on_progress = _progress
313
+ tasks.attach(sessions_dir / f"{session.session_id}.tasks.json" if sessions_dir else None)
314
+ tree.attach(tree.root_id(), session)
315
+ from rlmagent.playbook.improver import Improver
316
+ from rlmagent.plugins import rlm_agent_home
317
+ from rlmagent.refinement.controller import project_folder
318
+ from rlmagent.refinement.evidence import EvidenceLog
319
+
320
+ session.evidence = EvidenceLog(
321
+ None if no_session else project_folder(rlm_agent_home(), session.cwd) / "evidence.jsonl"
322
+ )
323
+ local = sessions_dir / f"{session.session_id}.playbook.json" if sessions_dir else None
324
+ session.improver = tree.improver = Improver(
325
+ session, local_path=local, shared_path=rlm_agent_home() / "playbook.json"
326
+ )
327
+ session.improver.auto = getattr(ns, "auto_improve", True)
328
+ session.improver.hold_auto = getattr(ns, "review_auto_improve", True)
329
+ session.improver.every = getattr(ns, "improve_every", 5)
330
+ session.improver.cooldown = getattr(ns, "improve_cooldown", 900.0)
331
+ session.on_compacted = lambda: (tree.tracer.compacted("root"), session.improver.compacted())
332
+ session.improver.show_digest()
333
+
334
+ max_turns = getattr(ns, "max_turns", None)
335
+ if max_turns is not None:
336
+ session.harness.settings.max_turns = max_turns
337
+
338
+ _install_hooks(session.harness, verbose=verbose)
339
+ _install_safety(session.harness, ApprovalPolicy.AUTO)
340
+ from rlmagent.refinement.controller import attach
341
+
342
+ try:
343
+ attach(session, rlm_agent_home(), persist=not no_session)
344
+ except Exception:
345
+ await session.shutdown()
346
+ raise
347
+ attach_logger(session)
348
+ session._apply_model_preferences()
349
+ return session
350
+
351
+
352
+ async def _resume_folder_problem(ns, session_id: str, sessions_dir) -> str | None:
353
+ """Why this session must not resume here: it was started for another project."""
354
+ from pathlib import Path
355
+
356
+ from rlmagent.harness.session.index import SessionCatalog
357
+
358
+ meta = SessionCatalog(sessions_dir).get(session_id)
359
+ if meta is None or not meta.cwd or getattr(ns, "resume_anywhere", False):
360
+ return None
361
+ if Path(meta.cwd).resolve() == Path(os.getcwd()).resolve():
362
+ return None
363
+ text = f"Session {session_id} belongs to {meta.cwd}, not {os.getcwd()}."
364
+ if not _is_interactive(ns):
365
+ return f"{text} Run it from that folder, or pass --resume-anywhere."
366
+ import asyncio
367
+
368
+ answer = await asyncio.get_running_loop().run_in_executor(
369
+ None, input, f"{text} Resume it here anyway? [y/N] ")
370
+ return None if answer.strip().lower().startswith("y") else "Resume cancelled."
371
+
372
+
373
+ def make_agent_tree(ns, provider, provider_name, model, sessions_dir, *, cwd, backend=False):
374
+ """Build the tree of agents a run uses, with the limits from the command line."""
375
+ from rlmness import Allowance
376
+
377
+ from rlmagent.agents.prompts import child_first_message, child_system
378
+ from rlmagent.agents.tree import AgentTree
379
+ from rlmagent.cli.main import _resolve_system
380
+
381
+ call_limit = getattr(ns, "max_calls", None)
382
+ if call_limit is None:
383
+ call_limit = 50 if str(model).startswith("acp:") else 200
384
+ allowance = Allowance(
385
+ max_depth=getattr(ns, "max_depth", 2),
386
+ max_calls=call_limit,
387
+ max_cost=getattr(ns, "max_cost", 2.0),
388
+ max_live=getattr(ns, "max_live", 8),
389
+ max_seconds=getattr(ns, "max_seconds", None),
390
+ )
391
+
392
+ from rlmagent.agents.approval import Approver
393
+ from rlmagent.kernel.wasm import available
394
+
395
+ kind = getattr(ns, "kernel", "local")
396
+ if kind == "wasm" and not available():
397
+ raise ValueError("The WebAssembly sandbox is not installed. Run `rlmness --setup` "
398
+ "or explicitly select --kernel local for host Python execution.")
399
+ network = getattr(ns, "network", True)
400
+ if getattr(ns, "auto_approve", False):
401
+ policy = "auto"
402
+ elif getattr(ns, "print_mode", False) or getattr(ns, "prompt", None) is not None:
403
+ policy = "deny"
404
+ else:
405
+ policy = "ask"
406
+
407
+ def system_for(node, tools):
408
+ base = _resolve_system(None, tools=tools, skills=[])
409
+ return child_system(base, node, node.depth < allowance.max_depth, sandboxed=kind == "wasm",
410
+ network=network)
411
+
412
+ if backend:
413
+ from rlmagent.backend_context import BackendContext
414
+ tree_type = BackendContext
415
+ else:
416
+ tree_type = AgentTree
417
+ tree = tree_type(
418
+ provider=provider,
419
+ provider_name=provider_name,
420
+ model=model,
421
+ cwd=cwd,
422
+ allowance=allowance,
423
+ sessions_dir=sessions_dir,
424
+ system_for=system_for,
425
+ first_message_for=child_first_message,
426
+ cell_timeout=getattr(ns, "cell_timeout", 300.0),
427
+ max_agents=getattr(ns, "max_agents", 50),
428
+ kernel=kind,
429
+ restore_kernel=getattr(ns, "restore_kernel", False),
430
+ approver=Approver(policy),
431
+ network=network,
432
+ )
433
+ tree.allow_private_fetch = getattr(ns, "allow_private_fetch", False)
434
+ import math
435
+ request_timeout = getattr(ns, "request_timeout", 300.0)
436
+ if not math.isfinite(request_timeout) or request_timeout <= 0:
437
+ raise ValueError("--request-timeout must be finite and positive")
438
+ tree.request_timeout = request_timeout
439
+ return tree
440
+
441
+
442
+ def _is_interactive(ns: argparse.Namespace) -> bool:
443
+ """A human is at a terminal and this run is not one-shot or piped."""
444
+ import sys
445
+
446
+ if getattr(ns, "print_mode", False) or getattr(ns, "prompt", None) is not None:
447
+ return False
448
+ return sys.stdin.isatty() and sys.stdout.isatty()
449
+
450
+
451
+ async def settle_project_trust(
452
+ ns: argparse.Namespace, *, ask: Asker | None = None,
453
+ ) -> TrustOutcome:
454
+ """Decide whether this folder's project inputs load, before any are read."""
455
+ from rlmagent.cli.main import _die, _info, _warn
456
+ from rlmagent.config.store import load_config
457
+ from rlmagent.trust import StartupCancelled, resolve_project_trust
458
+
459
+ override: bool | None = None
460
+ if getattr(ns, "approve", False):
461
+ override = True
462
+ elif getattr(ns, "no_approve", False):
463
+ override = False
464
+ if ask is None and _is_interactive(ns):
465
+ ask = ask_on_console
466
+ default = load_config().project_trust or "ask"
467
+
468
+ try:
469
+ outcome = await resolve_project_trust(
470
+ os.getcwd(), override=override, default=default, ask=ask, # type: ignore[arg-type]
471
+ )
472
+ except StartupCancelled:
473
+ _die("No project trust decision made; exiting.")
474
+ if outcome.notice:
475
+ _warn(outcome.notice)
476
+ if not outcome.trusted and outcome.inputs is not None and not outcome.inputs.empty:
477
+ _info(
478
+ f"Project inputs not loaded ({outcome.inputs.describe()}): {outcome.reason}. "
479
+ "Run with --approve to load them this once."
480
+ )
481
+ return outcome
482
+
483
+
484
+ def trust_choices(request: TrustRequest) -> list[tuple[Choice, str]]:
485
+ from rlmagent.trust import Choice
486
+
487
+ menu = [(Choice.TRUST_FOLDER, "Trust this folder")]
488
+ if request.parent is not None:
489
+ menu.append((Choice.TRUST_PARENT, f"Trust parent folder ({request.parent})"))
490
+ menu += [
491
+ (Choice.TRUST_ONCE, "Trust for this run only"),
492
+ (Choice.DISTRUST_FOLDER, "Do not trust this folder"),
493
+ (Choice.DISTRUST_ONCE, "Do not trust for this run only"),
494
+ ]
495
+ return menu
496
+
497
+
498
+ def trust_question(request: TrustRequest) -> str:
499
+ """The explanation shown above the choices, shared by every frontend."""
500
+ lines = [
501
+ f"{request.folder} contains project inputs: {request.inputs.describe()}.",
502
+ "They shape the agent's instructions and can add plugins that run as code.",
503
+ "This controls project inputs; it is not a sandbox.",
504
+ ]
505
+ if request.store_problem:
506
+ lines.append(f"Saved decisions are unavailable: {request.store_problem}")
507
+ return "\n".join(lines)
508
+
509
+
510
+ async def ask_on_console(request: TrustRequest) -> Choice | None:
511
+ """Ask on the terminal; EOF or Ctrl+C cancels."""
512
+ import asyncio
513
+ import sys
514
+
515
+ menu = trust_choices(request)
516
+ sys.stderr.write(trust_question(request) + "\n")
517
+ for number, (_choice, label) in enumerate(menu, start=1):
518
+ sys.stderr.write(f" {number}. {label}\n")
519
+ loop = asyncio.get_running_loop()
520
+ while True:
521
+ try:
522
+ answer = await loop.run_in_executor(None, input, f"Choose 1-{len(menu)}: ")
523
+ except (EOFError, KeyboardInterrupt):
524
+ return None
525
+ answer = answer.strip()
526
+ if answer.isdigit() and 1 <= int(answer) <= len(menu):
527
+ return menu[int(answer) - 1][0]
528
+
529
+
530
+ def attach_logger(session: CodingSession) -> None:
531
+ """Subscribe a ``RunLogger`` to the session's event stream."""
532
+ from rlmagent.logs import RunLogger
533
+
534
+ logger = RunLogger.create(
535
+ session.session_id,
536
+ provider=session.provider_name,
537
+ model=session.model,
538
+ cwd=session.cwd,
539
+ )
540
+ if logger is None:
541
+ return
542
+ logger.stats_source = lambda: session.usage
543
+ session.harness.on_event(logger.record_event)
544
+
545
+
546
+ def git_branch(cwd: str | None = None) -> str | None:
547
+ """Current git branch name, or ``None`` outside a repository."""
548
+ import subprocess
549
+
550
+ try:
551
+ out = subprocess.run(
552
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
553
+ cwd=cwd or os.getcwd(),
554
+ capture_output=True,
555
+ text=True,
556
+ timeout=2,
557
+ encoding="utf-8",
558
+ errors="replace",
559
+ )
560
+ except (OSError, subprocess.SubprocessError):
561
+ return None
562
+ if out.returncode != 0:
563
+ return None
564
+ branch = out.stdout.strip()
565
+ return branch or None
566
+
567
+
568
+ __all__ = [
569
+ "ask_on_console",
570
+ "build_session",
571
+ "git_branch",
572
+ "settle_project_trust",
573
+ "trust_choices",
574
+ "trust_question",
575
+ ]