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
@@ -0,0 +1,74 @@
1
+ """Frontend metadata and policies for a backend-owned execution tree."""
2
+ from types import SimpleNamespace
3
+
4
+ from rlmagent.agents.approval import Approver
5
+ from rlmagent.agents.budget import BudgetedProvider
6
+ from rlmagent.agents.trace import TracedProvider, Tracer
7
+ from rlmagent.model.external import with_external
8
+
9
+
10
+ class BackendContext:
11
+ def __init__(self, *, provider, provider_name, model, cwd, allowance,
12
+ sessions_dir, kernel="wasm", approver=None, network=False,
13
+ cell_timeout=300, max_agents=50, tracer=None, **metadata):
14
+ self.provider = BudgetedProvider(with_external(provider), allowance)
15
+ self.provider_name, self.model, self.cwd = provider_name, model, cwd
16
+ self.allowance, self.sessions_dir = allowance, sessions_dir
17
+ self.kernel_kind, self.network = kernel, network
18
+ self.approver = approver or Approver("deny")
19
+ self.cell_timeout, self.max_agents = cell_timeout, max_agents
20
+ self.tracer = tracer or Tracer(None)
21
+ self.improver = None
22
+ self.records, self.backend_agents = {}, {}
23
+ self.nodes = {"root": SimpleNamespace(context=None)}
24
+ self._finished = False
25
+ self.__dict__.update(metadata)
26
+
27
+ async def start(self):
28
+ pass
29
+
30
+ def root_id(self):
31
+ return "root"
32
+
33
+ def root_kernel(self):
34
+ # Tool metadata identifies the coding route. The solver owns execution.
35
+ return self
36
+
37
+ async def run(self, code, timeout=None):
38
+ raise RuntimeError("submit code through the backend solver")
39
+
40
+ def attach(self, agent, session):
41
+ self.session = session
42
+
43
+ def provider_for(self, agent):
44
+ return TracedProvider(self, agent)
45
+
46
+ def use_provider(self, provider, provider_name):
47
+ self.provider = BudgetedProvider(with_external(provider), self.allowance)
48
+ self.provider_name = provider_name
49
+ return self.provider
50
+
51
+ def _role(self, name):
52
+ if self.improver is None:
53
+ raise RuntimeError("playbook unavailable")
54
+ entry = self.improver.entry(name)
55
+ if entry['kind'] != 'role':
56
+ raise ValueError("entry is not a role")
57
+ return entry['content']
58
+
59
+ def child_session_ids(self, parent):
60
+ return []
61
+
62
+ def finish_trace(self, complete=True):
63
+ if self._finished:
64
+ return None
65
+ self._finished = True
66
+ agents = [{"agent": key, "parent": value.get("parent_run_id"),
67
+ "task": value.get("instruction"), "status": value.get("status"),
68
+ "session": getattr(getattr(self, "session", None), "session_id", None)
69
+ if key == "root" else None}
70
+ for key, value in self.backend_agents.items()]
71
+ return self.tracer.finish(agents, self.provider.refusal, complete)
72
+
73
+ async def close(self):
74
+ self.finish_trace()
@@ -0,0 +1,5 @@
1
+ """rlm-agent cli: the command-line entry point."""
2
+
3
+ from rlmagent.cli.main import get_version, main
4
+
5
+ __all__ = ["get_version", "main"]
@@ -0,0 +1,177 @@
1
+ """``rlm-agent config`` — interactive provider configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from collections.abc import Sequence
8
+
9
+ from rlmagent.config import prompts
10
+ from rlmagent.config.store import (
11
+ PROVIDERS,
12
+ config_path,
13
+ load_config,
14
+ mask_key,
15
+ provider_info,
16
+ save_config,
17
+ )
18
+
19
+
20
+ def _build_parser() -> argparse.ArgumentParser:
21
+ p = argparse.ArgumentParser(prog="rlm-agent config")
22
+ p.add_argument("--provider", default=None, help="Provider key (skips the menu).")
23
+ p.add_argument("--model", default=None, help="Model identifier (skips the menu).")
24
+ p.add_argument(
25
+ "--api-key", default=None,
26
+ help="API key (skips the prompt). Also read from RLM_AGENT_API_KEY.",
27
+ )
28
+ p.add_argument(
29
+ "--visible", action="store_true",
30
+ help="Type the API key visibly, for terminals without hidden input.",
31
+ )
32
+ sub = p.add_subparsers(dest="action")
33
+ sub.add_parser("show", help="Show the saved configuration (keys masked).")
34
+ sub.add_parser("path", help="Print the configuration file path.")
35
+ sub.add_parser("doctor", help="Report this terminal's hidden-input support.")
36
+ return p
37
+
38
+
39
+ def _doctor() -> int:
40
+ """Print terminal capability diagnostics."""
41
+ from rlmagent.config.secure_input import describe_terminal
42
+
43
+ prompts.info("Terminal hidden-input diagnostics:\n")
44
+ for line in describe_terminal():
45
+ prompts.info(f" {line}")
46
+ prompts.info(
47
+ "\nIf hidden input is unavailable, use one of:\n"
48
+ " rlm-agent config --visible\n"
49
+ " rlm-agent config --provider openai --model gpt-5 --api-key sk-...\n"
50
+ " $env:RLM_AGENT_API_KEY='sk-...'; rlm-agent config --provider openai"
51
+ )
52
+ return 0
53
+
54
+
55
+ def _save_noninteractive(
56
+ provider: str, model: str | None, api_key: str | None,
57
+ ) -> int:
58
+ """Save configuration without prompting."""
59
+ try:
60
+ info = provider_info(provider.strip().lower())
61
+ except KeyError:
62
+ valid = ", ".join(p.key for p in PROVIDERS)
63
+ prompts.info(f"Unknown provider {provider!r}. Choose one of: {valid}")
64
+ return 1
65
+
66
+ cfg = load_config()
67
+ key = (api_key or "").strip()
68
+ if not key and info.needs_key and info.key not in cfg.api_keys:
69
+ prompts.info(f"{info.label} requires an API key (--api-key or RLM_AGENT_API_KEY).")
70
+ return 1
71
+
72
+ cfg.provider = info.key
73
+ cfg.model = model or cfg.model or info.models[0]
74
+ if key:
75
+ cfg.api_keys[info.key] = key
76
+ path = save_config(cfg)
77
+
78
+ prompts.info(f"Configuration saved to {path}")
79
+ prompts.info(f"Provider: {info.label}")
80
+ prompts.info(f"Model: {cfg.model}")
81
+ return 0
82
+
83
+
84
+ #: Environment variables that take precedence over the saved file for a run.
85
+ OVERRIDES = ("RLM_AGENT_PROVIDER", "RLM_AGENT_MODEL", "RLM_AGENT_API_KEY", "RLM_AGENT_HOME")
86
+
87
+
88
+ def _show() -> int:
89
+ """Print the saved configuration with API keys masked, and what the environment overrides."""
90
+ import os
91
+
92
+ cfg = load_config()
93
+ path = config_path()
94
+ overrides = [(name, os.environ[name]) for name in OVERRIDES if os.environ.get(name)]
95
+ if not path.exists():
96
+ prompts.info(f"No configuration at {path}. Run 'rlm-agent config' to create one.")
97
+ else:
98
+ prompts.info(f"Config: {path}")
99
+ prompts.info(f"Provider: {cfg.provider or '(unset)'}")
100
+ prompts.info(f"Model: {cfg.model or '(unset)'}")
101
+ for name in sorted(cfg.api_keys):
102
+ prompts.info(f" {name}: {mask_key(cfg.api_keys[name])}")
103
+ if overrides:
104
+ prompts.info("Set in the environment, used instead of the file:")
105
+ for name, value in overrides:
106
+ shown = mask_key(value) if name.endswith("_KEY") else value
107
+ prompts.info(f" {name}={shown}")
108
+ return 0 if path.exists() or overrides else 1
109
+
110
+
111
+ def _configure(*, visible: bool = False) -> int:
112
+ """Run the interactive setup flow."""
113
+ cfg = load_config()
114
+
115
+ labels = [p.label for p in PROVIDERS]
116
+ default = next(
117
+ (i for i, p in enumerate(PROVIDERS) if p.key == cfg.provider), 0,
118
+ )
119
+ info = PROVIDERS[prompts.choose("Select provider", labels, default=default)]
120
+
121
+ prompts.info(f"\n{info.label}")
122
+
123
+ existing = cfg.api_keys.get(info.key)
124
+ key = prompts.ask_api_key(
125
+ info.label,
126
+ required=info.needs_key,
127
+ has_existing=bool(existing),
128
+ visible=visible,
129
+ )
130
+ if key is None:
131
+ key = existing or ""
132
+ if info.needs_key and not key.strip():
133
+ prompts.info("API key must not be empty. Nothing saved.")
134
+ return 1
135
+
136
+ current_model = cfg.model if cfg.provider == info.key else None
137
+ model = prompts.choose_model(list(info.models), current=current_model)
138
+
139
+ if not prompts.confirm("Save as default?"):
140
+ prompts.info("Nothing saved.")
141
+ return 1
142
+
143
+ cfg.provider = info.key
144
+ cfg.model = model
145
+ if key.strip():
146
+ cfg.api_keys[info.key] = key.strip()
147
+ path = save_config(cfg)
148
+
149
+ prompts.info(f"\nConfiguration saved to {path}")
150
+ prompts.info(f"Provider: {info.label}")
151
+ prompts.info(f"Model: {model}")
152
+ return 0
153
+
154
+
155
+ def handle_config(argv: Sequence[str]) -> int:
156
+ """Entry point for the ``config`` subcommand."""
157
+ ns = _build_parser().parse_args(list(argv))
158
+ if ns.action == "show":
159
+ return _show()
160
+ if ns.action == "path":
161
+ prompts.info(str(config_path()))
162
+ return 0
163
+ if ns.action == "doctor":
164
+ return _doctor()
165
+
166
+ api_key = ns.api_key or os.environ.get("RLM_AGENT_API_KEY")
167
+ if ns.provider and (api_key or ns.model):
168
+ return _save_noninteractive(ns.provider, ns.model, api_key)
169
+
170
+ try:
171
+ return _configure(visible=ns.visible)
172
+ except prompts.PromptAborted:
173
+ prompts.info("\nCancelled. Nothing saved.")
174
+ return 130
175
+
176
+
177
+ __all__ = ["handle_config"]
rlmagent/cli/health.py ADDED
@@ -0,0 +1,156 @@
1
+ """`rlm-agent status` and `rlm-agent doctor`: what is set up, what is running, what is stale."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import sys
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass
12
+ class Check:
13
+ name: str
14
+ ok: bool
15
+ detail: str
16
+ fix: str = "" # what doctor --fix would do; empty when it cannot fix this
17
+
18
+
19
+ def _config() -> Check:
20
+ from rlmagent.config.store import config_path, load_config
21
+
22
+ path = config_path()
23
+ if not path.exists():
24
+ env = next((n for n in ("OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
25
+ "RLM_AGENT_API_KEY") if os.environ.get(n)), None)
26
+ if env:
27
+ return Check("provider", True, f"no saved configuration; {env} is set")
28
+ return Check("provider", False, "no configuration; run rlm-agent to pick a provider, or rlm-agent config")
29
+ cfg = load_config()
30
+ has_key = bool(cfg.api_keys.get(cfg.provider or "", ""))
31
+ return Check("provider", bool(cfg.provider),
32
+ f"{cfg.provider or '(unset)'} / {cfg.model or '(unset)'}, "
33
+ f"key {'saved' if has_key else 'not saved (environment or none)'}")
34
+
35
+
36
+ def _sandbox() -> Check:
37
+ try:
38
+ from rlmness.wasm_runtime import wasm_available
39
+ except ImportError:
40
+ return Check("sandbox", False, "rlmness is not installed")
41
+ if shutil.which("node") is None:
42
+ return Check("sandbox", False, "Node.js 18+ is not on PATH; install it for --kernel wasm")
43
+ if not wasm_available():
44
+ return Check("sandbox", False, "Pyodide is not installed", "install Pyodide (rlmness --setup)")
45
+ return Check("sandbox", True, "Node and Pyodide found")
46
+
47
+
48
+ def _git() -> Check:
49
+ ok = shutil.which("git") is not None
50
+ return Check("git", ok, "found; /undo and /diff work" if ok else "not on PATH; /undo and /diff are off")
51
+
52
+
53
+ def _workers() -> list[Check]:
54
+ from rlmagent import worker
55
+
56
+ base = worker.root()
57
+ rows = []
58
+ for path in sorted(base.iterdir()) if base.is_dir() else []:
59
+ if not path.is_dir():
60
+ continue
61
+ meta = worker.state_of(path.name)
62
+ recorded = meta.get("state", "?")
63
+ if meta["alive"]:
64
+ rows.append(Check(f"worker {path.name}", True, f"{recorded}, session {meta.get('session')}"))
65
+ elif recorded in ("starting", "idle", "busy"):
66
+ rows.append(Check(f"worker {path.name}", False,
67
+ f"recorded as {recorded} but its process is gone", "mark it stopped"))
68
+ else:
69
+ rows.append(Check(f"worker {path.name}", True, recorded))
70
+ return rows
71
+
72
+
73
+ def _sessions_open() -> list[Check]:
74
+ from rlmagent.agentnet import _home, _read
75
+ from rlmagent.worker import _pid_alive
76
+
77
+ base = _home()
78
+ rows = []
79
+ for path in sorted(base.iterdir()) if base.is_dir() else []:
80
+ meta = _read(path / "meta.json")
81
+ if meta is None:
82
+ continue
83
+ if meta.get("pid") and _pid_alive(int(meta["pid"])):
84
+ rows.append(Check(f"session {path.name}", True, f"open in {meta.get('cwd')}"))
85
+ else:
86
+ rows.append(Check(f"session {path.name}", False, "registered but its process is gone",
87
+ "remove the registration"))
88
+ return rows
89
+
90
+
91
+ def _mcp() -> Check:
92
+ from rlmagent.mcp import load_configs
93
+ from rlmagent.plugins import rlm_agent_home
94
+
95
+ servers, problems = load_configs(rlm_agent_home(), None)
96
+ if problems:
97
+ return Check("mcp", False, "; ".join(problems))
98
+ return Check("mcp", True, f"{len(servers)} server(s) in the home mcp.json")
99
+
100
+
101
+ def _search() -> Check:
102
+ from rlmagent.websearch import SERVICES, configured
103
+
104
+ name = configured()
105
+ return Check("web search", True,
106
+ f"{name} is set" if name else f"off; set one of {', '.join(SERVICES)}")
107
+
108
+
109
+ def checks() -> list[Check]:
110
+ return [_config(), _sandbox(), _git(), _mcp(), _search(), *_workers(), *_sessions_open()]
111
+
112
+
113
+ def _fix(check: Check) -> str:
114
+ if check.name == "sandbox":
115
+ from rlmness import home
116
+
117
+ return "installed Pyodide" if home.setup() == 0 else "Pyodide install failed"
118
+ if check.name.startswith("worker "):
119
+ from rlmagent import worker
120
+
121
+ name = check.name.split(" ", 1)[1]
122
+ meta = worker.state_of(name)
123
+ meta.update(state="stopped", pid=None)
124
+ meta.pop("alive", None)
125
+ worker._write(worker.folder(name) / "meta.json", meta)
126
+ return "marked stopped"
127
+ if check.name.startswith("session "):
128
+ from rlmagent.agentnet import _home
129
+
130
+ (_home() / check.name.split(" ", 1)[1] / "meta.json").unlink(missing_ok=True)
131
+ return "removed"
132
+ return "nothing done"
133
+
134
+
135
+ def run(argv: list[str], *, doctor: bool) -> int:
136
+ from rlmagent.cli.main import get_version
137
+ from rlmagent.plugins import rlm_agent_home
138
+
139
+ fix = "--fix" in argv
140
+ out = sys.stdout
141
+ out.write(f"rlm-agent {get_version()}, home {rlm_agent_home()}\n")
142
+ found = checks()
143
+ problems = 0
144
+ for check in found:
145
+ mark = "ok " if check.ok else "!! "
146
+ out.write(f"{mark}{check.name}: {check.detail}\n")
147
+ if not check.ok:
148
+ problems += 1
149
+ if doctor and check.fix:
150
+ out.write(f" {'fixing: ' + _fix(check) if fix else 'doctor --fix would ' + check.fix}\n")
151
+ if doctor:
152
+ out.write("no problems found\n" if not problems else f"{problems} problem(s)\n")
153
+ return 1 if doctor and problems and not fix else 0
154
+
155
+
156
+ __all__ = ["Check", "checks", "run"]